Skip to content

About

Companion app for the campaign dashboard tutorial series on shopify.dev

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Campaign performance dashboard

An embedded Shopify app that measures a marketing campaign's sales, QR code traffic, and email, SMS, and push sends. It writes each campaign to the store's Analytics charts as annotations and targets.

The app queries sales, top products, creator codes, and customer segments with ShopifyQL. It reports each send to Shopify with App Events, then queries the deliveries, opens, and clicks beside the sales. Each QR code belongs to one campaign. The app registers each code as a marketing activity and tags every scan with the code's UTM parameters. Shopify then attributes the sessions and orders that follow a scan to the code, and the app reports each code's conversion rate and sales.

This repository holds the finished app. The finished app builds on the app that Build a Shopify app using React Router produces. A three-part tutorial series builds the dashboard inside that app, one capability per part.

  1. Query campaign sales covers the scopes, the ShopifyQL sales queries, the creator codes, and QR code attribution.
  2. Add targets and annotations sets revenue targets, marks the campaign on the store's analytics timeline, and groups sales by the dimensions your app owns.
  3. Report on a campaign with App Events reports your app's marketing events, then queries the funnel beside the sales.

Setup

You'll need Node 22.12 or later, app development permissions, a dev store, and the latest Shopify CLI.

  1. Install Shopify CLI:

    npm install -g @shopify/cli@latest
  2. Clone the repository and install dependencies:

    git clone https://github.com/Shopify/example-analytics--campaign-dashboard--react.git
    cd example-analytics--campaign-dashboard--react
    npm ci
  3. Create the app record and link this directory to it:

    npm run config:link
  4. Restore access_scopes.scopes in shopify.app.toml. config:link overwrites the line, and a new app record has no scopes to write. If you install the app with no scopes, every query and every write fails:

    [access_scopes]
    scopes = "write_reports,write_analytics_annotations,write_products,write_customers,write_orders,write_metaobject_definitions,write_metaobjects,write_marketing_events"
  5. Create the local database and start the app. Shopify CLI asks for a dev store and installs the app with the scopes from step 4. Press p to open the app:

    npm run setup
    npm run dev
  6. In a second terminal, deploy the QR code metaobject definition and the App Events declarations. Run the deploy after npm run dev starts, because until then the app URL is a placeholder and the deploy rejects the webhook addresses:

    npm run deploy

The app opens on a campaign list with two campaigns that the app seeds. Panels read $0 or show no rows until you request the access below and the store has orders. The event declarations from npm run deploy take a few minutes to propagate before an App Events query returns rows.

Tests

Run the unit tests, and check that every [START] tag the tutorials excerpt still has its [END] tag:

npm test
npm run check:markers

Access to request

Access What stays empty without it
Level 2 protected customer data The campaign detail page. shopifyqlQuery returns an access error, and a Couldn't load sales banner replaces the panels. The campaign list still loads.

Level 2 protected customer data

shopifyqlQuery requires Level 2 across all four protected fields, and the error names them: Level 2 access to Customer data including name, address, phone, and email fields. Request only Name and every sales panel stays empty.

Before you request access, select a distribution method for the app that config:link created. You can't change the method after you select it.

Then request access in the Partner Dashboard:

  1. From the Partner Dashboard, go to Apps, and then select your app.
  2. In the sidebar, click API access requests.
  3. Find Protected customer data access and click Request access.
  4. Select Protected customer data, provide your reasons for using it, and click Save.
  5. Select Name, Email, Phone, and Address, provide your reasons for using them, and click Save.

An app that's installed only on a dev store gets access after step 5, with no review.

API version

Three files pin version 2026-10. shopify.app.toml sets the webhooks API version, app/shopify.server.js sets the version for every GraphQL Admin API call, and .graphqlrc.js sets the version for GraphQL codegen. Keep all three files on the same version.

Credentials

This repository ships none. The client ID in shopify.app.toml is empty, and .gitignore excludes environment files. config:link writes your own client ID, app URL, and redirect URLs. shopify app dev supplies the API key and secret to the running process.

Never put a client secret in shopify.app.toml. It's a tracked file.

Use the dashboard

The first load seeds two planned campaigns. The app sets their dates relative to your install:

Campaign Window What it shows
Summer launch Opened 20 days ago, a week left Nine sends from 20 to 7 days ago. Step 5 of this list posts them.
Back to school Starts in two weeks Nothing to report. Every panel shows the setup it still needs.
  1. On the campaign list, under Analytics dimensions, click Create dimension on each of the three rows. Do this once. A report that groups on a dimension returns no rows until the dimension reads Queryable.
  2. The product row now reads Analytics off for Campaign. Click Turn on analytics on that row. The admin opens the Campaign type. Turn on Filter or group data in Analytics, then reload the app. Apps can't change this setting in API version 2026-10. The row reads Queryable about 30 seconds later.
  3. Open Summer launch. Under Campaign setup, click Select products and choose up to five, then choose a Customer segment.
  4. Click Activate campaign. Annotations and targets appear on the next render.
  5. Click Send events and confirm. The text above the button names the send and event counts. Posting takes a minute or two, and the funnel panels have numbers a few minutes after that.
  6. Click Open in Analytics to confirm the annotations landed outside the app.

Targets are optional. Activation creates a target for each amount you set and skips each blank one. The annotations go up either way.

Four more controls write to the same charts. Close campaign rewrites the annotation with what the campaign earned. If the order dimension is queryable, the close also records the last campaign touch before each order. Cancel campaign takes the annotations and targets back off the store. Replan campaign returns a canceled campaign to planned for a second activation, and Delete campaign removes it.

To try the QR code flow, click QR codes in the app's navigation and create a code. Under Campaign, pick a planned campaign, then save. A planned campaign's page also links to a new code with its campaign picked. A code belongs to one campaign, and the Campaign column on the list links to it. Live on sets the day the code's mark lands on the timeline. Blank goes live with the campaign. Activating the campaign marks each of its codes on the timeline. With a QR code target amount, activation also creates one target per code. Every scan redirects to the code's scan destination with the code's UTM parameters. The destination is the product page or checkout. The QR code sales panel reports the sessions each code started and the orders shoppers place after scanning.

Each campaign is an entry of the Campaign metaobject type, which the app creates on first load. The admin lists the entries with the store's other metaobjects, and users can edit the entries there. Re-cloning doesn't reset them. The seed runs once per store. Deleting the seeded campaigns doesn't bring them back. Each creator is an entry of the Creator type, and campaigns share those entries. Deleting a campaign leaves its creators in place.

Get numbers on the charts

A dev store with no orders reports $0. The seed creates campaigns. The sales come from orders you create.

  • A dev store accepts only test payments, and test orders don't reach the sales schema. Create orders with the GraphQL Admin API's orderCreate mutation instead, with a processedAt date inside Summer launch's window. The nine sends land between 20 and 7 days before you install.
  • Cluster the orders in the days right after a send, with the largest cluster in the last few days. If every order falls in the opening week, each later send reads as having moved nothing.
  • Creator revenue groups sales by discount_code, and that column counts a code only when a buyer enters it at checkout. QR code sales reads orders that follow a scan. Both panels need real checkouts on a store that accepts payments. On a dev store, Creator revenue lists each code at $0, and QR code sales lists each scanned code's sessions at $0.

What it does

An active campaign's page runs the panels in the order a campaign produces evidence. Targets come first, then sales against the period before and the same days last year, the send funnel per channel, the top products, revenue grouped by the dimensions this app creates, and a closing summary that the app writes back to the store's timeline.

Chips beside each panel title name what produced the number, whether ShopifyQL, App Events, metafields, annotations, targets, or web components.

The tutorial series walks through every panel and the query behind it.

Troubleshooting

What you see What it means
A Couldn't load sales banner replaces the sales panels Either the scopes line didn't survive config:link, or Level 2 access hasn't landed for this app. Reading live analytics needs both write_reports and Level 2.
Every sales panel reads $0 The store has no orders in the campaign's window.
A Campaign events aren't available yet banner npm run deploy hasn't propagated yet. Reload after a few minutes.
Column Not Found on an app_events query The declaration hasn't reached the store. Don't send events until it clears.
Creator revenue lists every code at $0 No sale has one of the three codes.
Promoted product sales is empty Analytics is off for the Campaign type, or the setting changed under a minute ago. Turn it on from the product row under Analytics dimensions.
A segment panel is empty The dimension isn't queryable yet. Create it from Analytics dimensions.
Order attribution is missing after a close Analytics takes a few minutes to index the order metafields the close wrote. Reload after that.
Two campaigns share a product or an order, and one loses its credit Each product holds one campaign reference and each order holds one campaign_touch value. The campaign that activates or closes last overwrites the other. Run one campaign per product at a time.
A campaign shows a status you didn't set Entries live on the store, and the admin can edit them. The clone holds none of them.

How it's built

The app starts from Build a Shopify app using React Router and adds three campaign routes:

Path File
/app/campaigns app/routes/app.campaigns._index.jsx
/app/campaigns/:id app/routes/app.campaigns.$campaignId.jsx
/app/campaigns/:id/stamping app/routes/app.campaigns.$campaignId.stamping.jsx

The QR code routes come from that tutorial: a list page, an edit form, and two public routes that serve each code's image and record each scan. app/models/QRCode.server.js holds their queries, and the codes live in an $app:qrcode metaobject that shopify.app.toml defines.

app/campaign-queries.server.js holds the GraphQL documents, app/campaigns.server.js reads and writes campaign entries, and app/app-events.server.js holds the App Events client and send plan. Each module under app/panels/ renders one group of panels. extensions/app-events/shopify.extension.toml declares the events. shopify.app.toml holds no credentials.

Each campaign is an entry of the campaign_dashboard_campaign metaobject type. ensureDefinition creates the type at runtime. In API version 2026-10, analytics groups only by a type without the $app: prefix. Each QR code's campaign field holds its campaign entry's ID as text, because a field in shopify.app.toml can't reference a type the app creates at runtime. An uninstall leaves every campaign entry in place. CAMPAIGN_FIELDS documents the shape field by field.

The app keeps its private records in its own SQLite database through Prisma, and app/campaign-state.server.js reads and writes them. The records hold the IDs of the annotations and targets that each campaign created, how many events each campaign sent, the last campaign ID the app handed out, and a short-lived lock per campaign. The shop/redact webhook deletes them 48 hours after an uninstall.

Creators are entries of the campaign_dashboard_creator type, and ensureDefinition creates that type first. The Campaign creators field validates against the Creator definition's ID, and a metaobject reference validation can't change after the field exists. A runtime type keeps one ID for the life of the store, unless someone deletes the Creator definition. CREATOR_FIELDS documents the Creator shape.

For a smaller starting point covering only the ShopifyQL half, see Build a sales dashboard with the GraphQL Admin API and example-shopifyql--sales-dashboard--react.

Contributions

This repository is a documentation sample. It doesn't accept issues or contributions. To report a problem with the tutorial, use the feedback control on the tutorial page.

License

The MIT License covers this repository.

About

Companion app for the campaign dashboard tutorial series on shopify.dev

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages