@shopify/reportify-react

Issues • Shipit

Usage no npm install needed!

<script type="module">
  import shopifyReportifyReact from 'https://cdn.skypack.dev/@shopify/reportify-react';
</script>

README

Reportify React

Build status

IssuesShipit

@shopify/reportify-react is a package that helps you fetch and manage data from Reportify in your React applications.

Getting Started

Install the package in your react project to get started:

  • yarn add @shopify/reportify-react

Bundled with reportify-react is a helper script called build-consumer. This allows for easy tophatting within Shopify repositories. Usage looks like:

yarn build-consumer PROJECT_DIRECTORY

Where PROJECT_DIRECTORY is the project you intend to test with the reportify-react changes. Because the primary consumer of reportify-react is web, here is a sample workflow of how to easily test changes in web:

Using dev (local development)

  1. Stop the web server.
  2. Run yarn run sewing-kit clean --cache in web to clear out any existing version of reportify-react.
  3. Run yarn build-consumer web within reportify-react. This will build the project and copy the new build to web.
  4. Start the web server

Using Spin

  1. Run yarn build-consumer web within reportify-react. This will build the project and copy the new build to web.
  2. Run restart shopify--web to restart the web server
  3. If the changes don't take, run rm -rf /src/github.com/shopify/web/build/esbuild/client and then try restart shopify--web again

There are 3 important parts of @shopify/reportify-react that you need to use to fetch data: ReportifyClient, ReportifyProvider, and useReportifyQuery. A quick sample follows this paragraph immediately, with more in-depth information below.

import {ReportifyClient, ReportifyProvider, useReportifyQuery} from '@shopify/reportify-react';

// Create the client
const client = new ReportifyClient({
  source: 'my-source',
  refetchToken: () => Promise.resolve('my-token'),
});

// Create a component
function TotalSales({start, end}) {
  const queryResult = useReportifyQuery(
    `SHOW total_sales FROM sales SINCE ${start} UNTIL ${end}`,
    {
      pollInterval: 5000,
    }
  );

  if (queryResult.isLoading) {
    return <p>Loading...</p>;
  } else if (queryResult.hasError) {
    console.log(queryResult.errorMessage);
    return <p style={{color: 'red'}}>There was an error! Check the console</p>;
  }

  return (
    <div>
      <p>Your total sales from {start} to {end} were {queryResult.response.result.data[0][0]}</p>
    </div>
  );
}

// Wrap your application in a ReportifyProvider
export default function App() {
  return (
    <ReportifyProvider client={client}>
      <TotalSales start="2017-01-01" end="today" />
    </ReportifyProvider>
  );
}

ReportifyClient

The Reportify client is the means by which you authenticate with and make requests to Reportify.

Options

Option Type Required? Description
source string The source parameter helps Reportify identify your client. It should describe your application. Ping #analytics-stack on Slack to find out more about the source param.
refetchToken (): Promise<string> refetchToken is a required function that tells the client how to fetch an auth token that can be used when querying Reportify. It takes no parameters and should return a Promise which resolves to a string.
hostName string Specify a Reportify endpoint to query. Default is https://analytics.shopify.com
token string You may provide an initial token to the client so that it does not need to call refetchToken on its first request.
maxRetries number Specify the maximum number of times the client should retry your query when receiving a failed response. Default is 1.

In addition to these options, you can call the following methods on the client once it is created:

Name Arguments Description
subscribeToQueryStatusEvents {id: string; callback: (QueryStatus) => void} Sets up a callback function that is called whenever query status events happen. useReportifyQueryStatus sets up this subscription internally, it is recommened you use that function instead of managing your own subscriber

ReportifyProvider

The Reportify provider gives its children access to the reportify client. In most cases, you will not work directly with the Reportify client – simply put it high in your component tree and pass it an instance of ReportifyClient.

Options

Option Type Required? Description
client ReportifyClient An instance of ReportifyClient that will be used to query Reportify

useReportifyQuery

useReportifyQuery is a hook that helps you fetch data using ShopifyQL queries you provide.

Options

useReportifyQuery receives a query as the first argument and an options object with the following available properties as its second argument:

Option Type Description
pollInterval number or function The interval at which you would like the hook to poll for new data. If passed a function, the function will receive pollCount (the number of times the component has polled), previousPollInterval (the last poll interval passed or computed), and previousResponses (the last two responses to be returned from Reportify, with most recent first)
source string The source parameter helps Reportify identify your client. This will override the source given to ReportifyClient/ReportifyProvider for just this query.
dataOnly boolean Determines whether the Reportify response should contain metadata or just data.
skip boolean Determines whether to skip the Reportify query. When skip is true, response is set to undefined, and data is set to an empty array.

ReportifyQueryOptionsProvider

Sometimes, it might be more convenient to share option values between multiple useReportifyQuery invocations. Passing options into the <ReportifyQueryOptionsProvider> will set some default values that can still be overridden in individual useReportifyQuery invocations.

Options

Option Type Description
source string The source parameter helps Reportify identify your client. This will override the source given to ReportifyClient/ReportifyProvider for any queries in this React component tree as long as options.source passed to useReportifyQuery is undefined.
dataOnly boolean Determines whether the Reportify response should contain metadata or just data. This will become the default behavior for any queries in this React component tree as long as options.dataOnly passed to useReportifyQuery is undefined.
import {ReportifyQueryOptionsProvider, useReportifyQuery} from '@shopify/reportify-react';

function TotalSales({start, end}) {
  // will use shared values from context
  // {dataOnly:true, source:'shared-source'}
  const queryOne = useReportifyQuery(
    `SHOW total_sales FROM sales SINCE ${start} UNTIL ${end}`
  );

  // will override the context values
  // {dataOnly:false, source:'a-new-source'}
  const queryTwo = useReportifyQuery(
    `SHOW total_sales FROM sales SINCE ${start} UNTIL ${end}`,
    {
      dataOnly: false,
      source: 'a-new-source'
    }
  );

  // ...
}

export default function Section() {
  return (
    <ReportifyQueryOptionsProvider options={{dataOnly: true, source: 'shared-source'}}>
      <TotalSales start="2017-01-01" end="today" />
    </ReportifyQueryOptionsProvider>
  );
}

useReportifyAST

useReportifyAST is a hook that fetches Reportify data formatted as an abstract syntax tree (AST).

Options

useReportifyAST receives a ShopifyQL query string as its first argument and an optional second argument in the form of an object with the following properties:

Option Type Description
source string The source parameter helps Reportify identify your client. This will override the source given to ReportifyClient/ReportifyProvider for just this query.

useReportifyAST returns an object containing the following parameters:

Option Type Description
ast Object An abstract syntax tree of the query results
loading boolean Represents whether or not the query is still loading
error boolean Represents whether or not the query returns an error

Example

const {ast, loading, error} = useReportifyAST('SHOW total_sales FROM sales SINCE -1d UNTIL -1d', {source: 'example'});

useReportifyQueryStatus

useReportifyQueryStatus is a hook provides the status of the queries fired by the client object. It provides a consistent status response for all of the queries that are requested as part of the initial render. This is useful when you have many queries firing at once and need a way to track when they have all resolved.

Return Value

useReportifyQueryStatus has three possible values:

  • undefined: This indicates that no queries have been sent.
  • 'pending': This indicates that queries have been sent and that the client is awaiting responses from Reportify.
  • 'stable': This indicates that the queries have resolved and there are no pending queries.
  • 'error-auth': This indicates that auth errors were returned to the queries, generally indicating an expired Minvera token. This error usually only occurs in development.
  • 'error-some': This indicates that only some of the queries errored out.
  • 'error-all': This indicates that all of the queries have errored out.

Caveats

If you are using multiple hooks that each poll, after the first render the status may appear to rapidly switch between pending and stable. This is due to the behavior of setTimeout within the client and the way that the event loop works in the browser. If you intend to use this within an application that polls with multiple hooks on the same page, consider debouncing the return value.

withReportify

withReportify is a decorator that wraps your components and provides data based on the ShopifyQL queries you provide it.

Options

withReportify receives a single options parameter with the following properties:

  • an optional source string that overrides the source options provided in the client.
  • a queries object. The queries object should have the name of the query as the key and an object with the following options as the value:
Option Type Required? Description
query string or function The query you want to send to Reportify. query accepts either a string or a function that will recieve the props passed from the parent component; the function should return a valid ShopifyQL string.
pollInterval number or function pollInterval is an optional parameter that defines how often react-reportify will poll for new data for the query being defined. It accepts either a number for a fixed interval or a function that receives the number of times the container has polled which should return a number as well as props passed from the parent component.
transform function transform is an optional parameter that defines how a ReportifyResponse will be transformed once the response is received. It should return a ReportifyResponse.
skip function skip is an optional parameter that allows for a query to be skipped. It is a function that receives props passed from the parent component; returning a truthy value will cause the query to be skipped and a falsey value will have normal behavior. Polling is not affected by skipping and timers will restart as soon as a query is determined to be in need of skipping.

Examples

Here are a few examples of options passed to withReportify:

Two queries

withReportify({
  queries: {
    totalSales: {
      query: 'SHOW total_sales FROM sales SINCE yesterday UNTIL today',
    },
    sessions: {
      query: 'SHOW total_sessions FROM visits SINCE yesterday UNTIL today',
    },
  },
});

Dynamic, polling query

withReportify({
  queries: {
    salesDateRange: {
      // `query` receives props passed into the component – in this case,
      // that might be something like <MyComponent start="2018-01-01" end="2018-01-31">
      query: ({start, end}) => `SHOW total_sales FROM sales SINCE ${start} UNTIL ${end}`,

      // Pass a function that slows down polling as time goes on
      pollInterval: (pollCount) => pollCount * 5000,
    },
  },
});

A query with a source

withReportify({
  source: 'my-custom-source',
  queries: {
    totalSales: {
      query: 'SHOW total_sales FROM sales SINCE yesterday UNTIL today',
    },
  },
});

<Query />

<Query /> is an alternative way to fetch data that relies on a render prop instead of wrapping your component.

Props

Prop Type Required? Description
queries {[handle: string]: string} The query that will be sent to Reportify.
source string The source parameter helps Reportify identify your client. This will override the source given to ReportifyClient/ReportifyProvider for just this query.
children (queryResults: ResultMap<ResponseShape>): React.ReactNode A render function that query results are passed to
combineUpdates boolean Wait for all queries to finish before passing data to children

withReportifyClient

Sometimes you will need direct access to the methods the client has to help query reportify. When that is the case, you can decorate your component with withReportifyClient.

withReportifyClient()(MyComponent);

In the example above, MyComponent will receive a new prop -- reportifyClient -- with the following methods:

Method Description
query(shopifyql: string, options?: {endpoint?: string, beta?: boolean, handle?: string, source?: string}): Promise<ReportifyResponse> Dispatch a reportify query. Calls to query will be batched, just like
withReportify.
exportQueryURL(shopifyql: string, filename: string): Promise<string> Create a URL that can be used to export a report for the given ShopifyQL query as a CSV.
fetchCSV(shopifyql: string): Promise<string> Returns a Blob containing the query result as a CSV

An example using exportQueryURL

class MyComponent extends React.Component {
  render() {
    return <button onClick={this.handleClick}>✨</button>;
  }

  handleClick = async () => {
    const {reportifyClient} = this.props; // `reportifyClient` comes from `withReportifyClient` below

    const exportUrl = await reportifyClient.exportQueryURL(
      'SHOW total_sales FROM sales SINCE -1d UNTIL today',
      'my_exported_report',
    );

    window.location = exportUrl;
  }
}

withReportifyClient()(MyComponent);

An example using fetchCSV

class MyComponent extends React.Component {
  render() {
    return 
    <>
      <button onClick={this.handleClick}>✨</button>
      <FileDownloader blob={exportBlob} filename={'test_file'} />
    </>
  }

  handleClick = async () => {
    const {reportifyClient} = this.props; // `reportifyClient` comes from `withReportifyClient` below

    const csvBlob = await reportifyClient.fetchCSV(
      'SHOW total_sales FROM sales SINCE -1d UNTIL today'
    );
    
    this.setState({
      exportBlob: csvBlob,
    });
  }
}

withReportifyClient()(MyComponent);

Testing Reportify React

Detail instructions around testing Reportify React can be found in the docs.