How to Monitor a Shopify Store with Playwright and Checkly

This is a guest post by Vince Graics, Staff QA Engineer at World of Books.

If you're running a Shopify storefront and want reliable synthetic monitoring, you'll hit a wall. Shopify's bot detection doesn't care that your headless browser is friendly; it sees datacenter IPs and acts accordingly. Cart API calls get hit with 429 rate limits, Cloudflare challenge pages pop up mid-check, and you're left wondering whether the bug is in your code or in the platform fighting you.

At World of Books, a UK-based secondhand bookseller running on Shopify, we ran into all of this while setting up monitoring with Checkly. The good news: Shopify actually has a fix for the bot-protection problem. The bad news: almost nobody talks about it, and there are a couple of non-obvious traps along the way.

This guide covers how we solved three Shopify-specific problems: bot protection headers, consent pop-up interception, and checkout flow testing. The patterns here work for any Shopify store. Swap out the selectors, plug in your store's URLs, and you have a working setup to monitor your Shopify store with Playwright.

What makes Shopify stores hard to monitor?

Shopify stores introduce platform-level obstacles that standard Playwright tests don't account for.

Bot protection. Shopify evaluates incoming traffic based on IP reputation, headers, and behavioral patterns. Monitoring checks run from cloud infrastructure, and datacenter IPs get treated with suspicion. Sometimes you get a 429 Too Many Requests on a cart API call. Sometimes Cloudflare intercepts the request entirely and serves a challenge page. And sometimes everything looks like it loaded, the page shell renders, the layout is there, but the interactive parts are quietly broken. A search page shows zero product cards. A mini-cart notification never appears.

Privacy and consent interception. Shopify's privacy SDK fires on every page load, showing a cookie consent banner and blocking script execution until the user accepts. In a headless context, that banner just sits there, adding flakiness and wasted seconds to every run.

Checkout DOM isolation. Shopify's checkout runs on checkout.shopify.com with a completely different markup. The selectors you wrote for the storefront don't work on checkout, and the page structure can change between Shopify editions.

Each section below tackles one of these problems and builds on the solution.

How do you bypass Shopify bot protection?

Shopify provides a mechanism for authorized crawlers to identify themselves: HTTP signature headers. You register your crawler in the Shopify admin, and Shopify gives you three headers: Signature, Signature-Input, and Signature-Agent.

When your requests include these headers, Shopify's bot protection lets them through cleanly.

Creating your crawler signature in Shopify

From your Shopify admin:

  1. Go to Online Store > Preferences
  2. In the Crawler access section, click Create signature
  3. Enter a descriptive name (e.g., "Checkly Monitoring")
  4. Select the domain and an expiration period
  5. Copy the Signature-Input and Signature values

The third header, Signature-Agent, is static; its value is always "<https://shopify.com>" (with quotes).

One thing to watch for: signatures have an expiration date. Set a calendar reminder to rotate them before they expire, or your checks will start failing again with zero code changes. When they expire, every check returns a challenge page and it looks like your entire store is down.

Storing headers as Checkly secrets

Add the three values as secrets in your Checkly account:

  • SHOPIFY_SIGNATURE
  • SHOPIFY_SIGNATURE_INPUT
  • SHOPIFY_SIGNATURE_AGENT

This keeps your credentials out of code and makes them available to all your browser checks at runtime.

Injecting the headers in Playwright

The simplest approach, and what you should try first:

await page.setExtraHTTPHeaders({
  'Signature': process.env.SHOPIFY_SIGNATURE!,
  'Signature-Input': process.env.SHOPIFY_SIGNATURE_INPUT!,
  'Signature-Agent': process.env.SHOPIFY_SIGNATURE_AGENT!,
});

setExtraHTTPHeaders adds these headers to every request the page makes. For many Shopify stores, this just works and you're done.

When you need scoped header injection

If your store uses third-party services that don't play nice with extra headers, setExtraHTTPHeaders can cause problems. At World of Books, we hit this with a third-party search provider: the extra headers on cross-origin requests appeared to interfere with its responses, and product listings silently came back empty. No errors, no warnings; just an empty product grid and a check that fails for reasons that take an hour to debug.

The fix is Playwright's page.route(), which scopes header injection to your own domain:

const crawlerHeaders: Record<string, string>
  = (process.env.SHOPIFY_SIGNATURE
    && process.env.SHOPIFY_SIGNATURE_INPUT
    && process.env.SHOPIFY_SIGNATURE_AGENT)
    ? {
        'Signature': process.env.SHOPIFY_SIGNATURE,
        'Signature-Input': process.env.SHOPIFY_SIGNATURE_INPUT,
        'Signature-Agent': process.env.SHOPIFY_SIGNATURE_AGENT,
      }
    : {};

await page.route('**/www.yourstore.com/**', route =>
  route.continue({
    headers: { ...route.request().headers(), ...crawlerHeaders },
  }),
);

Third-party requests pass through untouched. A few things to note:

All three headers are required together. If any are missing, skip the injection entirely; partial headers won't work with Shopify's validation. And spread existing headers first: route.continue() replaces headers entirely, so the spread preserves cookies, content types, and everything else the browser set. After your test, call page.unrouteAll({ behavior: 'ignoreErrors' }) to avoid route handler leaks between checks.

Scaling this with Playwright fixtures

Once you have more than a couple of checks, you don't want to repeat the header setup in every file. Playwright fixtures let you extend the base page object once and use it everywhere:

import { test as base } from '@playwright/test';

export const test = base.extend({
  page: async ({ page }, use) => {
    if (Object.keys(crawlerHeaders).length > 0) {
      await page.route('**/www.yourstore.com/**', route =>
        route.continue({
          headers: { ...route.request().headers(), ...crawlerHeaders },
        }),
      );
    }
    await use(page);
    await page.unrouteAll({ behavior: 'ignoreErrors' });
  },
});

Every check that imports test from this fixture gets crawler header injection automatically. No headers configured? The fixture is a no-op; your checks still work locally without Shopify credentials.

The consent banner problem is frustrating because it's invisible in manual testing (you click it once and forget it) but it breaks every automated check.

Playwright's page.addLocatorHandler() lets you register a handler that fires whenever a matching element appears. Think of it as a popup bouncer for your checks:

export const test = base.extend({
  page: async ({ page }, use) => {
    await page.addLocatorHandler(
      page.locator('[aria-label="Close popup"]'),
      async () => {
        await page.locator('[aria-label="Close popup"]').click();
      },
    );
    await use(page);
  },
});

This works well for stores with a single, consistent popup. If your store has multiple overlays (newsletter signups, region selectors), you can register additional handlers with different locators. The key is keeping the selectors stable; aria-label attributes tend to survive theme updates better than CSS classes.

How do you test Shopify checkout?

Checkout is where monitoring gets tricky, because you're crossing into checkout.shopify.com with its own DOM, its own selectors, and real money involved.

Setting up safe checkout testing with Shopify Flow

You can't just skip checkout monitoring; it's the flow that matters most to your business. But running real transactions in production sounds terrifying. Here's the approach my team at World of Books came up with, using Shopify Flow:

  1. Get a company card
  2. Create a dedicated test user on your production store
  3. Set up a Flow automation: if this specific user purchases something, the workflow immediately refunds the order and cancels the trade
  4. Run your checkout checks against this test user

You can now run regular checkout validations until you run out of the company card budget, and wait for the refunds. It's not elegant, but it works, and it tests the real checkout flow, not a staging approximation.

Deploying to Checkly

Once the checks pass locally, deploy with the Checkly CLI:

# Test against Checkly infrastructure first
checkly test --record

# Deploy
checkly deploy -f -o

Set your environment variables in the Checkly dashboard under the check group. The required ones: SHOPIFY_SIGNATURE, SHOPIFY_SIGNATURE_INPUT, SHOPIFY_SIGNATURE_AGENT. Add LOGIN_EMAIL, LOGIN_PASSWORD, and discount codes if you're monitoring authenticated or promotional flows.

Because everything lives in your repo as code, your Shopify monitoring ships through the same CI/CD pipeline as your application. That's the Monitoring as Code approach: your checks are versioned, reviewed in PRs, and deployed automatically. No clicking through a UI to update a threshold.

Troubleshooting common failures

Challenge page instead of storefront. Your crawler headers are missing or expired. Regenerate them in Shopify Admin and update the env vars in Checkly.

Login fails or loops. Test account credentials are wrong, the account is locked, or MFA got enabled. Use a dedicated test account that the store admin has allowlisted.

Discount code shows "not valid." The code expired, hit its usage limit, or targets the wrong region. Verify it in Shopify Admin > Discounts. Use unlimited test codes.

Checks pass locally, fail on Checkly. Almost always missing environment variables. Run checkly env ls to verify. Second most common cause: viewport mismatch. Set viewport: { width: 1920, height: 1080 } explicitly in your Playwright config.

Wrapping up

At World of Books, customer experience is the reason we monitor our Shopify storefront so closely. If search breaks, checkout stalls, or a pop-up blocks the flow, it's important that we know before our customers do. Shopify can be difficult to test with headless browsers, but once we added crawler-signature headers via Checkly secrets, used a Playwright fixture to centralize header injection and consent handling, and set up Shopify Flow to clean up checkout test orders, the setup became reliable. The checks now see the same storefront our customers see, without the 429s, Cloudflare challenges, or quiet failures that make Shopify monitoring so frustrating.

If your checks keep mysteriously failing, check your headers.

Get started:

Related Articles