Skip to main content

Content Security Policy Setup

To enable Glood pixel tracking, you need to configure your Content Security Policy (CSP) to whitelist Glood’s domains. This allows the SDK to send analytics events to Glood endpoints.

Required CSP Configuration

Add the following Glood domains to your connectSrc directive in your CSP configuration:
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  connectSrc: [
    'https://cdn.shopify.com',
    'https://*.shopifycloud.com',
    'https://events.glood.ai',
    'https://s-pixel.glood.ai',
    'https://w-pixel.glood.ai'
  ],
});

Glood Domains Explained

Required Pixel Endpoints

DomainPurposeUsed By
https://events.glood.aiRecommendations pixel trackingRecommendations app
https://s-pixel.glood.aiSearch analytics trackingSearch app
https://w-pixel.glood.aiWishlist analytics trackingWishlist app

Why These Domains Are Needed

  • Event Transmission: The SDK sends analytics events to these endpoints via fetch() API calls
  • Privacy Compliance: Events are only sent when customer consent is granted
  • Performance Tracking: Enables monitoring of user interactions and e-commerce metrics
  • Personalization: Allows Glood to provide personalized recommendations and search results

Implementation in Hydrogen

1. Root Layout Configuration

Add the CSP configuration to your root layout file:
// app/root.tsx
import {
  json,
  type LoaderFunctionArgs,
} from '@shopify/remix-oxygen';
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
} from '@remix-run/react';
import {
  Analytics,
  getShopAnalytics,
  useNonce,
} from '@shopify/hydrogen';
import { createContentSecurityPolicy } from '@shopify/hydrogen';

export async function loader({context}: LoaderFunctionArgs) {
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    connectSrc: [
      'https://cdn.shopify.com',
      'https://*.shopifycloud.com',
      'https://events.glood.ai',         // Recommendations pixel
      'https://s-pixel.glood.ai',        // Search pixel
      'https://w-pixel.glood.ai'         // Wishlist pixel
    ],
  });

  return json({
    nonce,
    header,
    // ... other loader data
  });
}

export default function App() {
  const nonce = useNonce();
  const data = useLoaderData<typeof loader>();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Analytics>
          <GloodProvider client={glood} loaderData={data}>
            <Layout>
              <Outlet />
            </Layout>
          </GloodProvider>
        </Analytics>
        <ScrollRestoration nonce={nonce} />
        <Scripts nonce={nonce} />
      </body>
    </html>
  );
}

export function headers({loaderHeaders}: {loaderHeaders: Headers}) {
  return {
    'Content-Security-Policy': loaderHeaders.get('Content-Security-Policy'),
  };
}

2. Environment-Specific Configuration

Configure different endpoints for different environments:
export async function loader({context}: LoaderFunctionArgs) {
  const isDevelopment = context.env.NODE_ENV === 'development';
  const isStaging = context.env.NODE_ENV === 'staging';

  const gloodDomains = isDevelopment
    ? [
        'https://dev-events.glood.ai',
        'https://dev-s-pixel.glood.ai',
        'https://dev-w-pixel.glood.ai'
      ]
    : isStaging
    ? [
        'https://staging-events.glood.ai',
        'https://staging-s-pixel.glood.ai',
        'https://staging-w-pixel.glood.ai'
      ]
    : [
        'https://events.glood.ai',
        'https://s-pixel.glood.ai',
        'https://w-pixel.glood.ai'
      ];

  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    connectSrc: [
      'https://cdn.shopify.com',
      'https://*.shopifycloud.com',
      ...gloodDomains
    ],
  });

  return json({ nonce, header });
}

3. Custom Domain Configuration

If you’re using custom Glood endpoints:
export async function loader({context}: LoaderFunctionArgs) {
  const customEndpoints = [
    context.env.GLOOD_RECOMMENDATIONS_PIXEL_ENDPOINT,
    context.env.GLOOD_SEARCH_PIXEL_ENDPOINT,
    context.env.GLOOD_WISHLIST_PIXEL_ENDPOINT,
  ].filter(Boolean); // Remove undefined values

  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    connectSrc: [
      'https://cdn.shopify.com',
      'https://*.shopifycloud.com',
      ...customEndpoints
    ],
  });

  return json({ nonce, header });
}

CSP Directives Explained

connectSrc

The connectSrc directive controls which URLs can be loaded using script interfaces:
  • fetch() - Used by the SDK to send pixel events
  • XMLHttpRequest - Alternative HTTP request method
  • WebSocket - Real-time connections (not used by Glood)
  • EventSource - Server-sent events (not used by Glood)

Why Not Other Directives?

DirectiveNot RequiredReason
scriptSrcGlood doesn’t load external scripts
imgSrcGlood doesn’t load tracking pixels as images
frameSrcGlood doesn’t embed iframes
mediaSrcGlood doesn’t load media files

Troubleshooting CSP Issues

1. Check Console Errors

CSP violations appear in the browser console:
Content Security Policy: The page's settings blocked the loading of a resource at https://events.glood.ai/

2. Verify Domain Configuration

Ensure all Glood domains are included:
// ❌ Missing domains will cause CSP violations
connectSrc: [
  'https://cdn.shopify.com',
  'https://events.glood.ai', // Only recommendations pixel
  // Missing search and wishlist pixels!
],

// ✅ All required domains included
connectSrc: [
  'https://cdn.shopify.com',
  'https://*.shopifycloud.com',
  'https://events.glood.ai',      // Recommendations
  'https://s-pixel.glood.ai',     // Search
  'https://w-pixel.glood.ai'      // Wishlist
],

3. Test with Debug Mode

Enable debug mode to see pixel transmission attempts:
const glood = createGlood({
  apiKey: process.env.GLOOD_API_KEY!,
  myShopifyDomain: 'your-store.myshopify.com',
  debug: true, // Enable debug logging
});
Debug logs will show:
[Glood Debug] Sending event to https://events.glood.ai (attempt 1)
[Glood Debug] Successfully sent event to https://events.glood.ai
If CSP is blocking requests, you’ll see network errors instead.

4. Validate CSP Headers

Check that CSP headers are being sent:
# Check CSP header in response
curl -I https://your-store.com | grep -i content-security-policy

# Expected output:
Content-Security-Policy: default-src 'self'; connect-src 'self' https://cdn.shopify.com https://*.shopifycloud.com https://events.glood.ai https://s-pixel.glood.ai https://w-pixel.glood.ai;

Advanced CSP Configuration

1. Strict CSP with Nonces

Use nonces for enhanced security:
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  defaultSrc: ["'none'"],
  scriptSrc: [`'nonce-${nonce}'`, "'strict-dynamic'"],
  connectSrc: [
    "'self'",
    'https://cdn.shopify.com',
    'https://*.shopifycloud.com',
    'https://events.glood.ai',
    'https://s-pixel.glood.ai',
    'https://w-pixel.glood.ai'
  ],
  styleSrc: [`'nonce-${nonce}'`, "'unsafe-inline'"],
  imgSrc: ["'self'", "data:", "https:"],
});

2. Report-Only Mode

Test CSP changes without blocking requests:
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  connectSrc: [
    'https://cdn.shopify.com',
    'https://*.shopifycloud.com',
    'https://events.glood.ai',
    'https://s-pixel.glood.ai',
    'https://w-pixel.glood.ai'
  ],
  reportOnly: true, // Report violations without blocking
});

3. Domain Wildcards

Use wildcards for subdomain flexibility:
connectSrc: [
  'https://cdn.shopify.com',
  'https://*.shopifycloud.com',
  'https://*.glood.ai', // Wildcard for all Glood subdomains
],
⚠️ Note: Wildcards are less secure than explicit domain lists.

Security Considerations

1. Principle of Least Privilege

Only include domains that are actually needed:
// ✅ Good - Only required domains
connectSrc: [
  'https://events.glood.ai',
  'https://s-pixel.glood.ai',
  'https://w-pixel.glood.ai'
],

// ❌ Avoid - Overly permissive
connectSrc: [
  'https://*.glood.ai',    // Too broad
  'https://*',             // Far too broad
  '*'                      // Completely insecure
],

2. Environment Separation

Use different domains for different environments:
// Development: Use dev domains
'https://dev-events.glood.ai'

// Staging: Use staging domains
'https://staging-events.glood.ai'

// Production: Use production domains
'https://events.glood.ai'

3. Regular Review

  • Review CSP regularly to ensure it’s still appropriate
  • Remove unused domains when disabling Glood apps
  • Monitor CSP violations in production
  • Test changes in staging environments first

Integration Checklist

  • Add Glood domains to connectSrc directive
  • Include all required pixel endpoints:
    • https://events.glood.ai (recommendations)
    • https://s-pixel.glood.ai (search)
    • https://w-pixel.glood.ai (wishlist)
  • Configure environment-specific domains if needed
  • Test CSP with debug mode enabled
  • Verify no CSP violations in browser console
  • Monitor pixel transmission success in debug logs
  • Deploy CSP headers correctly
  • Test in production environment

Common Issues

Issue: CSP Blocking Pixel Requests

Symptoms: Network errors, no pixel events sent Solution: Add missing Glood domains to connectSrc

Issue: CSP Header Not Applied

Symptoms: CSP violations not shown, headers missing Solution: Ensure CSP headers are returned from loader and applied in headers function

Issue: Wrong Environment Domains

Symptoms: 404 errors on pixel endpoints Solution: Verify correct domains for current environment

Issue: Wildcard Domain Issues

Symptoms: Inconsistent blocking behavior Solution: Use explicit domain lists instead of wildcards

Example Implementation

Complete working example:
// app/root.tsx
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Analytics, createContentSecurityPolicy} from '@shopify/hydrogen';
import {GloodProvider, createGlood} from '@glood/hydrogen';
import {recommendations, search, wishlist} from '@glood/hydrogen';

const glood = createGlood({
  apiKey: process.env.GLOOD_API_KEY!,
  myShopifyDomain: process.env.PUBLIC_STORE_DOMAIN!,
  debug: process.env.NODE_ENV === 'development',
})
  .use(recommendations())
  .use(search())
  .use(wishlist());

export async function loader({context}: LoaderFunctionArgs) {
  const {nonce, header} = createContentSecurityPolicy({
    shop: {
      checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
      storeDomain: context.env.PUBLIC_STORE_DOMAIN,
    },
    connectSrc: [
      'https://cdn.shopify.com',
      'https://*.shopifycloud.com',
      'https://events.glood.ai',
      'https://s-pixel.glood.ai',
      'https://w-pixel.glood.ai'
    ],
  });

  return json({
    nonce,
    header,
    analytics: getShopAnalytics({
      storefront: context.storefront,
      publicStorefrontId: context.env.PUBLIC_STOREFRONT_ID,
    }),
  });
}

export default function App() {
  const {analytics} = useLoaderData<typeof loader>();

  return (
    <html>
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Analytics analytics={analytics}>
          <GloodProvider client={glood} loaderData={data}>
            <Outlet />
          </GloodProvider>
        </Analytics>
        <Scripts />
      </body>
    </html>
  );
}

export function headers({loaderHeaders}: {loaderHeaders: Headers}) {
  return {
    'Content-Security-Policy': loaderHeaders.get('Content-Security-Policy'),
  };
}
This configuration ensures that:
  • ✅ All Glood pixel endpoints are whitelisted
  • ✅ CSP headers are properly applied
  • ✅ Pixel tracking works correctly
  • ✅ Security is maintained with explicit domain lists

See Also