SDK Documentation

Pixel SDK

Pixel Installation

Install the Adverfly tracking pixel on your website

The Adverfly Pixel is a JavaScript snippet that tracks user interactions on your website. It captures pageviews, events, and conversions to power your analytics.

Quick Start

Add the following code to the <head> section of your website:

Request
<script>
  window.adverfly = window.adverfly || [];
  function advPxl() {
    var args = [false];
    for (var i = 0; i < arguments.length; i++) {
      args.push(arguments[i]);
    }
    adverfly.push(args);
  }
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.store_currency = "EUR";
  window.adverfly.store_timezone = "Europe/Berlin";

  var script = document.createElement("script");
  script.type = "text/javascript";
  script.async = true;
  script.src = "https://sos-de-fra-1.exo.io/adv/advv4.01.js";
  document.getElementsByTagName("head")[0].appendChild(script);
</script>

Replace YOUR_WORKSPACE_ID with your Workspace ID from the Adverfly dashboard.

Configuration

ParameterTypeRequiredDescription
workspace_idnumberRequiredYour Adverfly Workspace ID
store_currencystringRequiredYour store's base currency (e.g., EUR, USD)
store_timezonestringRequiredYour store's timezone (e.g., Europe/Berlin)

Currency Conversion

The store_currency setting is important for accurate revenue tracking. If a transaction comes in with a different currency (e.g., a customer pays in USD), Adverfly automatically converts it to your store currency using the current exchange rate.

Example: Your store currency is EUR. A customer pays $129 USD. Adverfly converts this to ~€119 EUR in your reports.

Timezone

The store_timezone ensures all events and conversions are recorded in your local time, making your reports easier to read and analyze.

Verify Your Installation

Install the Adverfly Pixel Helper Chrome extension to confirm your pixel is firing. The helper captures every beacon the pixel sends, decodes the payload, and shows the event in real time — works on regular sites and inside Shopify Web Pixels.

Tracking Events

Track events and conversions with the Adverfly Pixel

Everything you send through the pixel — pageviews, add-to-carts, purchases, custom events — lands in a single unified events table. The advPxl call has two event types:

  • "event" — interactions without a monetary outcome (add to cart, checkout start, custom)
  • "conversion" — revenue-bearing or goal events (purchase, lead)

Same wire format, same table; the type just controls how the event is treated downstream (attribution windows, dispatch to ad platforms, etc.).

Custom Events

Request
advPxl("event", "add_to_cart");
advPxl("event", "initiated_checkout");

Standard Events

CodeDescription
pageviewUser views a page (tracked automatically on init)
add_to_cartUser adds item to cart
initiated_checkoutUser starts checkout process
purchaseRevenue event — send via advPxl("conversion", "purchase", {...})
leadLead event — send via advPxl("conversion", "lead", {...})

Purchase Conversion

Request
advPxl("conversion", "purchase", {
  transaction_id: "order123",              // required
  transaction_gross_revenue: 10999,        // required (cents)
  transaction_currency: "EUR",             // required
  transaction_shipping_costs: 499,         // optional (cents)
  transaction_tax: 1900,                   // optional (cents)
  transaction_city: "Berlin",              // optional
  transaction_country_code: "DE",          // optional
  transaction_discount_code: "SUMMER20",   // optional
  customer_id: "customer@email.com",       // optional (hashed server-side)
  is_new_customer: 1,                       // optional (1 = new, 0 = returning)
});

Conversion Parameters

ParameterTypeRequiredDescription
transaction_idstringRequiredUnique order ID
transaction_gross_revenuenumberRequiredTotal revenue in cents (e.g., 10999 = €109.99)
transaction_currencystringRequiredCurrency code (EUR, USD, …)
transaction_shipping_costsnumberOptionalShipping cost in cents
transaction_taxnumberOptionalTax amount in cents
transaction_citystringOptionalCustomer's city
transaction_country_codestringOptionalISO country code (DE, US, …)
transaction_discount_codestringOptionalApplied discount/coupon code
customer_idstringOptionalCustomer identifier (e.g., email — SHA-256 hashed server-side)
is_new_customernumberOptional1 = new, 0 = returning

Line Items

Request
advPxl("conversion", "purchase", {
  transaction_id: "order123",
  transaction_gross_revenue: 2000,
  transaction_currency: "EUR",
  transaction_items: [
    {
      transaction_item_id: "123",
      transaction_item_name: "Product 1",
      transaction_item_price: 1000,
      transaction_item_tax: 50,
      transaction_item_quantity: 2,
    },
    {
      transaction_item_id: "124",
      transaction_item_name: "Product 2",
      transaction_item_price: 500,
      transaction_item_quantity: 1,
    },
  ],
});
ParameterTypeRequiredDescription
transaction_item_idstringRequiredProduct / SKU ID
transaction_item_namestringRequiredProduct name
transaction_item_pricenumberRequiredItem price in cents
transaction_item_quantitynumberRequiredQuantity
transaction_item_taxnumberOptionalItem tax in cents

Lead Conversion

Request
advPxl("conversion", "lead", {
  transaction_id: "lead-456",
});

Custom Properties

Any field you pass that isn't one of the reserved keys above is automatically merged into the event's properties JSON (server-side) — usable for filters, breakdowns, and ad-platform CAPI dispatch.

Two equivalent ways to send custom data:

Request
// Inline — non-reserved keys auto-merged into properties
advPxl("conversion", "purchase", {
  transaction_id: "order123",
  transaction_gross_revenue: 10999,
  transaction_currency: "EUR",

  // Anything non-reserved → events.properties JSON
  payment_provider: "klarna_invoice",
  shipping_method: "standard",
  affiliation: "DACH Store",
});

// Or explicit — the `properties` key wins on conflict
advPxl("event", "add_to_cart", {
  properties: {
    product_category: "skincare",
    cart_size: 4,
  },
});

Reserved keys (everything in the parameter tables above plus store_id, event_type, name, customer_id, transaction_*, utm_*, adv_*, form_id, …) stay top-level. Everything else lands in properties.

Query later via JSONExtractString(properties, 'payment_provider') in ClickHouse, or surface as a breakdown in any dashboard.

Autocapture

The pixel can capture user interactions automatically — clicks, form submits, input changes, rage clicks, dead clicks, and copy/cut actions — without manual advPxl calls. Disabled by default: opt in per workspace by setting window.adverfly.activate_autocapture = true before init.

Request
window.adverfly = window.adverfly || [];
window.adverfly.activate_autocapture = true;
advPxl("init", 8397799);

Captured Events

CodeDescription
$clickFired on clicks. Target is resolved to the nearest interactive ancestor (a, button, input, select, textarea, label, form, or role=button|link|menuitem).
$submitFired on form submits. Includes form action and method.
$changeFired on <select> changes and checkbox/radio toggles. Text input values are NEVER captured.
$rageclickFired when the same element is clicked 3 or more times within 1 second.
$dead_clickFired when a click on a link or button changes nothing at all — no DOM change, no navigation, no scroll, nothing selected — within 2.5 seconds. The classic signal for a broken button.
$copyFired when a user copies content. Only metadata is captured — the copied text itself is never captured.
$cutFired when a user cuts content. Only metadata is captured.

Privacy

Autocapture is designed to never leak sensitive data. The following are enforced client-side before anything is sent:

  • Input types never captured: password, hidden, file.
  • Sensitive fields blocked by name / autocomplete: credit card (cc-*, card-num, card-no), CVC/CVV, expiry, SSN, social, password, API keys, auth tokens, one-time-codes.
  • Freeform input values never read: <input type="text|email|tel|..."> and <textarea> values are never included. Only <select> options and checkbox/radio state are captured.
  • Text scrubbing: any token in visible text that looks like a credit card number, SSN, or a run of 13+ digits is stripped before sending. Result is truncated to 200 characters.
  • Opt-out selectors: elements (and their descendants) marked with class adv-no-capture or attribute data-adv-no-capture are skipped entirely.

Excluding Elements

Exclude a specific element (and all its children) from autocapture:

Request
<!-- Class-based -->
<div class="adv-no-capture">
  <input type="text" name="internal-note" />
</div>

<!-- Attribute-based -->
<section data-adv-no-capture>
  <button>Internal action</button>
</section>

Configuration

Fine-grained control via window.adverfly.autocapture_config (set before init):

CodeDescription
url_allowlistArray of strings (substring match) or RegExp. If set, autocapture runs only on URLs matching at least one entry.
url_ignorelistArray of strings or RegExp. Autocapture is skipped on URLs matching any entry.
element_allowlistArray of lowercase tag names. Only elements with these tags are captured.
css_selector_allowlistArray of CSS selectors. Only elements matching at least one selector are captured.
Request
window.adverfly = window.adverfly || [];
window.adverfly.autocapture_config = {
  url_ignorelist: [/\/admin/, "/debug"],
  css_selector_allowlist: [".track-me", "[data-adv-track]"],
};
advPxl("init", 8397799);

Rate Limiting

Built-in limits protect your site and your event quota:

  • Global: max 30 autocapture events per second.
  • Per element: minimum 500 ms between events on the same element.
  • Rage click: 1 s cooldown per element after a $rageclick fires.
  • Dead click: max 20 reports and 60 watched clicks per page load, at most four watched at a time.

What is never judged as a dead click

A click is only called dead when a working control would visibly have done something. These do their job without touching the page, so judging them would invent broken buttons for a shop that has none:

  • form fields and anything inside a <form>, plus editable content
  • a link with download, with target="_blank", or pointing at mailto:, tel: or sms:
  • a <canvas>, where a configurator draws pixels rather than nodes
  • a custom element or anything rendering into a shadow root, which cannot be observed from outside
  • anything that is not a link, button, summary or element with an interactive role, so ordinary body text is never reported

You can exempt anything else with data-adv-no-dead-click on the element or any ancestor.

One honest limitation: aliveness means "something on the page changed". A page with a countdown or an auto-rotating carousel changes constantly, so dead clicks are not detected there at all. The check errs towards silence rather than towards fabricating reports.

Pageviews in a single-page app

A React, Vue or Svelte storefront reports one pageview per hard navigation and nothing for the twenty screens the visitor actually went through. One flag fixes that:

Request
window.adverfly = window.adverfly || [];
window.adverfly.activate_spa_pageviews = true;
advPxl("init", 8397799);

The pixel then hooks the History API and fires a pageview with navigation: "spa" whenever the path changes.

Only the path, plus the hash when it is a route (#/products/socks), so hash-routed apps are counted too. A plain anchor like #reviews is not a screen. Filter and sort controls usually only touch the query string, and they are the classic way an automatic pageview hook runs away with itself: two shops on this platform sent up to 46,386 pageviews from a single visitor in one day because something pushed history state in a loop. If your routing genuinely lives in the query string, opt in:

Request
window.adverfly.spa_track_query = true;

Three more guards apply, and none of them are configurable:

  • The same path is never reported twice in a row.
  • Changes are debounced by 300 ms, so a router that replaces state three times during one transition produces one pageview.
  • After 100 pageviews in one page load the hook switches itself off and says so once in the console, rather than flooding silently.

On a Shopify storefront the Web Pixel already owns the event stream, so this hook stays off there even when the flag is set. Counting both would double every screen.

Loyalty Balance Email

If you run an Adverfly loyalty program, customers can request their current balance by email from anywhere in the shop (footer link, customer account page, exit-intent CTA, …). The pixel ships the request to Adverfly; we look the member up, render a branded HTML email with their balance + last 20 transactions, and deliver it via SES from your verified sending domain.

Disabled by default — opt in per shop by setting window.adverfly.activate_loyalty = true before init. This prevents accidental balance mails on shops that don't run a loyalty program.

Request
window.adverfly = window.adverfly || [];
window.adverfly.activate_loyalty = true;
advPxl("init", 8397799);

Drop-in template

Copy-paste this anywhere in your shop (footer, customer-account page, exit-intent modal). It includes the opt-in, a button, the click handler, and a confirmation message. No external dependencies.

Request
<!-- Adverfly Loyalty — Balance check
     Two ways to use this:
       1. Drop the <a> below into your footer / account page.
       2. OR add href="#adv-balance" to ANY existing link in your shop —
          the script below catches it globally. -->
<a href="#adv-balance"
   style="color:inherit;text-decoration:underline;cursor:pointer;">
  Mein Guthaben anzeigen
</a>

<script>
  (function () {
    window.adverfly = window.adverfly || [];
    window.adverfly.activate_loyalty = true;

    function trigger() {
      var email = (window.adverfly && window.adverfly.customer_id)
        || prompt("Bitte gib deine E-Mail-Adresse ein:");
      if (!email) return;
      advPxl("balance", { email: email });
      alert("Danke! Falls deine Adresse bei uns hinterlegt ist, erhältst du dein Guthaben in wenigen Minuten per E-Mail.");
    }

    document.addEventListener("click", function (e) {
      var t = e.target;
      if (!t || !t.closest) return;
      var link = t.closest('a[href$="#adv-balance"], #adv-loyalty-balance');
      if (!link) return;
      e.preventDefault();
      trigger();
    });
  })();
</script>

Logged-in shoppers

If your platform already identifies the customer (Shopify customer accounts set window.adverfly.customer_id to the email), omit the email arg — the pixel uses the known identity automatically.

Request
window.adverfly.customer_id = "kunde@example.com"; // typically set by the platform
advPxl("balance"); // no second arg needed

Shopify Liquid

If you're on a Shopify theme, you can render the button conditionally so guests still see a prompt while logged-in customers skip the input step.

Request
<button onclick="advPxl('balance', { email: '{{ customer.email | default: '' }}' || prompt('E-Mail eingeben:') })">
  Mein Guthaben anzeigen
</button>
<script>
  window.adverfly = window.adverfly || [];
  window.adverfly.activate_loyalty = true;
</script>

Behaviour

  • Always returns 200 — the response never reveals whether the email is on file (anti-enumeration).
  • No on-page feedback — pixel doesn't surface success / error. Show your own "Wenn du Mitglied bist, kommt die Mail in wenigen Minuten" message after the click.
  • Bounce-protected — addresses on the SES bounce/complaint list are silently skipped to protect your sending reputation.
  • From-address — uses your verified domain if configured (Loyalty → Email Sender), otherwise falls back to the Adverfly default.

Test from the dashboard

In Loyalty → Settings → Preview balance email, type an existing member email and click "Send to me". You'll receive a copy of the exact email that member would get, labeled [Test] with a yellow banner so it can't be confused with a live send. Useful for QAing design changes without triggering real customer mail.

Feature Reference

Every pixel feature, the flag that enables it, and what it costs

The pixel ships as one bundle, but almost nothing in it runs until you switch it on. A default install tracks pageviews, conversions and attribution, and does nothing else. Everything below is opt-in, per site, with one line in your snippet.

<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", YOUR_WORKSPACE_ID);

  window.adverfly.activate_autocapture = true;
  window.adverfly.activate_web_vitals = true;
</script>

Flags must be set before the bundle finishes loading, which is why they belong in the same inline block as advPxl("init", …) and not in a script that runs later.

Features

FlagTurns onWhat it costs
activate_autocaptureClicks, form submissions, rage clicks and dead clicks, without tagging anythingOne event per interaction
activate_ab_testingA/B tests, rollouts and the visual editorOne cached config request per visitor
activate_heatmapClick and scroll heatmapsBatched, one request per pageview
activate_session_replaySession recordingsThe largest of them all. Sample it, see Heat Maps & Session Replays
activate_web_vitalsCore Web VitalsOne event per pageview
activate_error_trackingUncaught JavaScript errors and rejected promises. Reporting one yourself with advPxl("exception", err) needs no flagOne event per distinct error, capped at 10 per pageview
activate_spa_pageviewsA pageview on every client-side route changeOne event per route change
activate_widgetsPopups, banners, toasts and surveys rendered by the pixelOne cached config request per visitor
activate_loyaltyThe logged-in loyalty area and the balance modalRequests only once a customer is signed in
activate_upsellPost-purchase and in-cart offersOne config request on the pages that use it

Identity and context

PropertyWhat it does
store_currencyISO 4217 code for the amounts you send, for example "EUR"
store_timezoneIANA zone, for example "Europe/Berlin". Drives which day an event lands on
customer_idYour own customer identifier. It travels as you set it and is hashed server-side before storage, so send an identifier you are comfortable putting on the wire
transaction_idThe current order, attached to survey and upsell events
consent_level"denied", "functional" or "marketing". Set it only if you resolve consent yourself

The pixel detects Cookiebot, Usercentrics, OneTrust and Consentmanager on its own. Setting consent_level manually overrides that detection, so set it only when you own the decision.

Tuning

PropertyDefaultWhat it does
activate_offline_queuefalsePark events that failed while the browser was offline and replay them later. Off because a replay lands on the day it arrives, see Data Quality
spa_track_queryfalseCount an SPA pageview when only the query string changes. Off, because filter and sort parameters are the classic source of pageview loops
extra_bot_patternsnoneExtra user-agent patterns to treat as bots, as strings or regular expressions
disable_bot_detectionfalseCount automated traffic instead of discarding it. See Data Quality
replay_confignoneSampling and masking for session replay
is_devfalsePoint the pixel at the development stack

Turning a feature off again

Remove the line. Nothing is remembered server-side: the flag is read on every page load, so a deploy that drops it stops the collection immediately. Data already collected stays.

Verifying

In the browser console on any page that loads the pixel:

window.adverfly                  // object, not undefined
window.adverfly.activate_heatmap // true when the flag reached the pixel
document.querySelector('script[src*="advv4"]')  // the bundle's script tag

If a flag reads undefined although it is in the snippet, it was set after the bundle read it. Move it up, into the same block as advPxl("init", …).

Google Tag Manager

Install Adverfly via Google Tag Manager

You can install the Adverfly Pixel using Google Tag Manager for easier management.

Installation Steps

  1. Open your GTM container
  2. Create a new Tag
  3. Choose Custom HTML
  4. Paste the following code:
Request
<script>
  window.adverfly = window.adverfly || [];
  function advPxl() {
    var args = [false];
    for (var i = 0; i < arguments.length; i++) {
      args.push(arguments[i]);
    }
    adverfly.push(args);
  }
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.store_currency = "EUR";
  window.adverfly.store_timezone = "Europe/Berlin";

  var script = document.createElement("script");
  script.type = "text/javascript";
  script.async = true;
  script.src = "https://sos-de-fra-1.exo.io/adv/advv4.01.js";
  document.getElementsByTagName("head")[0].appendChild(script);
</script>
  1. Set the trigger to All Pages
  2. Save and publish

Configuration

CodeDescription
store_currencyYour store's base currency. Transactions in other currencies are auto-converted.
store_timezoneYour store's timezone for accurate event timestamps in reports.

Tracking Conversions via Data Layer

Quick version — minimal mapping from the GA4-style purchase event:

Request
<script>
  // Fires on the purchase confirmation page
  advPxl("conversion", "purchase", {
    transaction_id: {{DL - Transaction ID}},
    transaction_gross_revenue: {{DL - Revenue}} * 100,
    transaction_currency: {{DL - Currency}}
  });
</script>

Full dataLayer Mapping

Most shops (Shopware, Shopify GTM, WooCommerce GA4) push a richer purchase event into the dataLayer. The pattern below subscribes to it and maps every useful field — line items, payment provider, shipping method, country, hashed email.

Drop this into a Custom HTML tag with the All Pages trigger; it auto-handles all future purchases without per-page tags.

Request
<script>
  if (window.dataLayer) {
    function advForwardPurchase(event) {
      if (!event || event.event !== "purchase") return;
      var ec = event.ecommerce || {};

      // "9.99" / 9.99 → 999 (cents). toFixed handles float drift,
      // replace strips the decimal point.
      var toCents = function (v) {
        return Number(v || 0).toFixed(2).toString().replace(/\D/g, "") || 0;
      };

      // pageCountryCode is shipped as "de_DE" — take the country part.
      var pageCC = (window.dataLayer || [])
        .map(function (e) { return e && e.pageCountryCode; })
        .filter(Boolean).pop() || "";
      var countryCode = pageCC.split("_").pop().toUpperCase().slice(0, 2);

      advPxl("conversion", "purchase", {
        // Reserved keys → typed columns
        transaction_id: ec.transaction_id || event.transaction_id,
        transaction_gross_revenue: toCents(ec.value || event.value),
        transaction_currency: ec.currency || event.currency,
        transaction_tax: toCents(ec.tax || event.tax),
        transaction_shipping_costs: toCents(ec.shipping || event.shipping),
        transaction_country_code: countryCode,
        customer_id: event.transactionEmail || ec.customer_id || "",

        transaction_items: (ec.items || []).map(function (it) {
          return {
            transaction_item_id: it.item_id || it.id,
            transaction_item_name: it.item_name || it.name,
            transaction_item_price: toCents(it.price),
            transaction_item_quantity: Number(it.quantity || 1),
          };
        }),

        // Non-reserved keys → events.properties JSON (auto-merged server-side)
        payment_provider: event.transactionPaymentType || "",
        shipping_method: event.transactionShippingMethod || "",
        affiliation: event.transactionAffiliation || "",
      });
    }

    // Hook every future push + replay anything already in the queue
    var originalPush = window.dataLayer.push;
    window.dataLayer.push = function () {
      var args = [].slice.call(arguments);
      originalPush.apply(this, args);
      args.forEach(advForwardPurchase);
    };
    window.dataLayer.forEach(advForwardPurchase);
  }
</script>

The pattern is the same for add_to_cart, begin_checkout, or any other dataLayer event — switch on event.event and call advPxl("event", "<name>").

Tracking Custom Events

Create additional tags for custom events:

Request
<script>
  // Add to Cart tag - trigger on add_to_cart event
  advPxl("event", "add_to_cart");
</script>
Request
<script>
  // Initiated Checkout tag - trigger on checkout start
  advPxl("event", "initiated_checkout");
</script>
CodeDescription
Base PixelAll Pages
Purchase Conversionpurchase event / thank you page
Add to Cartadd_to_cart event
Initiated Checkoutinitiated_checkout event

Shopify Integration

Install Adverfly on your Shopify store

Adverfly integrates natively with Shopify's Customer Events API for accurate tracking. If you also want to render personalization widgets (popups, banners, countdowns, toasts) on your storefront, a second tiny step is required — see "Optional: Enable Widgets" at the bottom of this page.

Installation Steps

  1. Go to your Shopify Admin
  2. Navigate to Settings → Customer events
  3. Click Add custom pixel
  4. Name it "Adverfly"
  5. Paste the following code:
Request
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = "https://sos-de-fra-1.exo.io/adv/script-shopify.js";
document.getElementsByTagName("script")[0].parentNode.appendChild(script);

window.adverfly_web_pixel = true;
window.adverfly_init = init;
window.adverfly_browser = browser;
window.adverfly_settings = api.settings;

window.adverfly = window.adverfly || [];
window.advPxl = function () {
  adverfly.push([false, ...arguments]);
};

analytics.subscribe("all_events", (event) => {
  window.adverfly.push(["all_shopify_events", event]);
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.store_currency = "EUR";
  window.adverfly.store_timezone = "Europe/Berlin";

  if (event.name === "page_viewed") {
    advPxl("check", "vikeys", event);
  }
});

Replace YOUR_WORKSPACE_ID with your Workspace ID and set your store's currency and timezone.

Configuration

CodeDescription
store_currencyYour store's base currency. Transactions in other currencies are auto-converted.
store_timezoneYour store's timezone for accurate event timestamps in reports.

Tracked Events

The Shopify integration automatically tracks all standard e-commerce events:

CodeDescription
page_viewed → pageviewShopify page_viewed event, recorded as pageview in Adverfly
product_added_to_cart → add_to_cartShopify add to cart event, recorded as add_to_cart in Adverfly
checkout_started → initiated_checkoutShopify checkout start, recorded as initiated_checkout in Adverfly
checkout_completed → purchaseShopify purchase event, recorded as purchase conversion in Adverfly

Debug Your Pixel

Shopify Web Pixels run inside a sandboxed iframe, so the browser's standard Network tab can be a pain to inspect. The Adverfly Pixel Helper Chrome extension captures every beacon the pixel sends — including from inside the Shopify sandbox — and shows the decoded event payload in a popup. Recommended for verifying the install before going live.

Optional: Enable Widgets

The Shopify Web Pixel above runs in a sandboxed iframe and can therefore track events but cannot render UI in your storefront. To enable Adverfly personalization widgets (popups, banners, countdowns, toasts), add a second tiny snippet to your theme — same pattern as Google Analytics or any other tag.

  1. In your Shopify Admin, open Online Store → Themes → ⋯ → Edit code
  2. Open layout/theme.liquid
  3. Paste this just before </head>:
Request
<script async src="https://cdn.adverfly.com/preset-pixel-adv.js"></script>
<script>
  window.adverfly = window.adverfly || [];
  window.adverfly.activate_widgets = true;
  advPxl("init", YOUR_WORKSPACE_ID);
</script>

The theme pixel auto-detects that you're on Shopify and skips firing pageview / vikey events — the Web Pixel above already tracks those. The theme pixel exists solely to render widgets in the storefront DOM. No double-counting.

If you only want tracking (no widgets), skip this section. The Web Pixel covers tracking on its own.

npm Package

Install the pixel from npm with full TypeScript types

@adverfly/pixel is a typed wrapper for applications that build their own JavaScript: Next.js, Nuxt, Remix, SvelteKit, a headless storefront. Merchants on Shopify, WooCommerce or a hand-written theme do not need it. The snippet from Pixel Installation does the same job.

The package contains no copy of the pixel. It loads the bundle from the CDN, exactly as the snippet does, so a fix reaches your site without a dependency bump. What it adds is the typed surface, safe server-side rendering, and a queue that accepts calls before the bundle has loaded.

No dependencies. Around 1 KB in your bundle.

Install

npm install @adverfly/pixel

Use

Request
import { init, track, conversion } from "@adverfly/pixel";

init(YOUR_WORKSPACE_ID, {
  currency: "EUR",
  timezone: "Europe/Berlin",
  features: { abTesting: true, autocapture: true, webVitals: true },
});

track("added_to_cart", { sku: "A-1", value: 2990 });

conversion("purchase", {
  transaction_id: order.id,
  transaction_gross_revenue: 9900,   // minor units, so 99.00 EUR
  transaction_currency: "EUR",
  is_new_customer: 1,
});

Amounts are always in minor units. The platform stores and computes in cents and converts once, for display.

Next.js

init does nothing during a server render, so the call is safe anywhere. With the App Router it belongs in a client component mounted by the root layout:

"use client";
import { useEffect } from "react";
import { init } from "@adverfly/pixel";

export function Analytics() {
  useEffect(() => {
    init(Number(process.env.NEXT_PUBLIC_ADVERFLY_ID), {
      currency: "EUR",
      features: { spaPageviews: true, errorTracking: true },
    });
  }, []);
  return null;
}

With the Pages Router the same call goes in pages/_app.tsx.

Turn on spaPageviews in any framework that routes on the client. Without it the pixel reports one pageview per hard navigation and nothing for the screens in between.

API

FunctionWhat it does
init(workspaceId, options?)Configures the pixel and loads it. Safe to call twice and during SSR
track(name, properties?)A custom event
conversion(name, payload)A revenue-bearing event. Amounts in minor units
pageview(properties?)A pageview. Only needed when spaPageviews is off
identify(customerId)Attaches your customer id to the events this package sends from here on. It travels as you set it and is hashed server-side before storage
captureException(error)Reports a handled error deliberately
consentChanged(level?)Tells the pixel consent changed, so it re-evaluates and catches up
survey(surveyId)Renders a survey by id
isBot()Whether this browser is considered automated and is therefore not counted

Options

Everything from the Feature Reference is available, in camelCase:

init(YOUR_WORKSPACE_ID, {
  currency: "EUR",
  timezone: "Europe/Berlin",
  consentLevel: "marketing",
  customerId: "customer@example.com",
  trackQueryChanges: false,
  extraBotPatterns: ["mycrawler"],
  disableBotDetection: false,
  features: {
    widgets: false,
    abTesting: false,
    sessionReplay: false,
    heatmap: false,
    autocapture: false,
    webVitals: false,
    errorTracking: false,
    spaPageviews: false,
    loyalty: false,
  },
  scriptUrl: "https://sos-de-fra-1.exo.io/adv/advv4.01.js",
  skipScript: false,
  nonce: undefined,
  isDev: false,
});

Three of these are not feature flags:

  • scriptUrl points the loader at a different bundle. Use it when you proxy the pixel through your own domain to survive ad blockers that filter on hostname.
  • skipScript configures the pixel without injecting anything. Use it when your theme already carries the install snippet and you only want the typed calls.
  • nonce is put on the injected script tag. Required on any site whose Content Security Policy uses nonces for script-src, which most Next.js apps with a strict policy do. Without it the browser blocks the tag and the pixel never loads.

If you gate the pixel behind consent, call init only after consent is given, or call it immediately with consentLevel: "denied" and call consentChanged("marketing") later. The second shape is usually better: the pixel is already loaded when consent arrives, so nothing is lost to the time it takes to fetch a script.

init(YOUR_WORKSPACE_ID, { consentLevel: "denied" });

onConsent((granted) => consentChanged(granted ? "marketing" : "denied"));

Ordering

Calls made before init are not lost. They queue in window.adverfly and run once the bundle arrives, in the order they were made. An install snippet already in your theme is reused rather than replaced, so the package and the snippet can coexist during a migration.

Personalization SDK

Headless JS SDK to render Adverfly widgets with your own components

The @adverfly/sdk JavaScript package lets you fetch the right personalized variant for the current visitor and render it however you want — your own React/Vue/Svelte components, server-rendered HTML, or even mobile apps.

The standard tracking pixel still works as a no-code option. Use the SDK when you need:

  • Pixel-perfect brand control — no iframe, no CSS overrides
  • Server-side / Edge personalization — Cloudflare Workers, Vercel Edge, Next.js Server Components
  • Custom UI — inline blocks, mobile screens, anything beyond popup/banner/toast/countdown
  • Strict typing — your config shape becomes a TypeScript generic

Install

Three formats, same code — pick whichever fits your stack.

Vanilla <script> (no build step)

Request
<script src="https://cdn.jsdelivr.net/npm/@adverfly/sdk/dist/adverfly.iife.js"></script>
<script>
  const adv = new Adverfly({ workspaceId: 188334 });
  /* `Adverfly` is now a global class — no module system required. */
</script>

Pin a version in production: @adverfly/sdk@0.1.0/dist/adverfly.iife.js.

ES modules in the browser

Request
<script type="module">
  import { Adverfly } from "https://cdn.jsdelivr.net/npm/@adverfly/sdk/dist/index.mjs";
  const adv = new Adverfly({ workspaceId: 188334 });
</script>

npm (build pipelines, Node, SSR)

Request
npm install @adverfly/sdk
import { Adverfly } from "@adverfly/sdk";

Works in browsers and Node 18+. Zero peer dependencies. Bundle is ~6 KB minified for the IIFE build, ~12 KB for the ESM build.

Quick start

Request
import { Adverfly } from "@adverfly/sdk";

const adv = new Adverfly({ workspaceId: 188334 });

await adv.identify({ email: "user@example.com" });

adv.setContext({
  cart_value: 49.9,
  last_viewed_creatives: ["sku_a", "sku_b"],
});

const variant = await adv.personalize({ trigger: "exit_intent" });

if (variant) {
  showMyPopup({
    title: variant.config.title,
    copy: variant.config.copy,
    onCtaClick: () => adv.click(variant.id),
    onDismiss: () => adv.dismiss(variant.id),
  });
  await adv.trackImpression(variant.id);
}

Constructor

ParameterTypeRequiredDescription
workspaceIdnumberRequiredYour Adverfly workspace ID.
apiUrlstringOptionalOverride for self-hosted or staging. Defaults to https://b.adverfly.com.
customerIdstringOptionalPre-identify without calling identify().
debugbooleanOptionalMirror decisions + events to console.log under [adverfly].
manualIdentitybooleanOptionalSkip auto-anonymous-id (for SSR / when you manage identity).

Identity

await adv.identify({
  email: "user@example.com",   // hashed (SHA-256, lowercased) → never sent raw
  transactionId: "order_123",  // optional, for post-purchase triggers
});

To reset identity (e.g. on logout):

adv.reset();

Personalize

const variant = await adv.personalize<MyConfigShape>({
  trigger: "exit_intent",
  surface: "widget",  // optional, defaults to "widget"
  context: { device_battery_low: true },  // merged on top of session context
});
// { id, config, reason } | null

Strongly-typed config:

Request
interface PopupConfig {
  title: string;
  copy: string;
  cta?: string;
  cta_url?: string;
  image_url?: string;
}

const variant = await adv.personalize<PopupConfig>({ trigger: "exit_intent" });
if (variant) {
  console.log(variant.config.title); // typed!
}

Tracking

All tracking events are written to the same ClickHouse pixel_events table the standard pixel uses — joinable by customer_id (hashed) for conversion attribution.

CodeDescription
trackImpression(variantId)Widget rendered into your UI.
click(variantId, props?)User clicked the CTA. props can include cta_url.
dismiss(variantId)User closed the widget manually.
autoDismissed(variantId, reason?)Timer expired (countdown, toast).
success(variantId, props?)Goal completed. If props.email is set, auto-hashed before send.

Events

Subscribe to the lifecycle for analytics, debugging, or custom rendering hooks.

Request
const off = adv.on("decision", ({ trigger, variant }) => {
  console.log(`[${trigger}] →`, variant?.id ?? "no match");
});

/* Returns an unsubscribe function */
off();

Available events: decision, impression, click, dismiss, auto_dismissed, success, error.

React

A first-class @adverfly/react package is planned. For now you can wrap the core SDK in a hook:

Request
import { Adverfly, type Variant } from "@adverfly/sdk";

const AdverflyContext = createContext<Adverfly | null>(null);

export function usePersonalization<T>(trigger: string) {
  const adv = useContext(AdverflyContext)!;
  const [variant, setVariant] = useState<Variant<T> | null>(null);

  useEffect(() => {
    adv.personalize<T>({ trigger }).then((v) => {
      setVariant(v);
      if (v) adv.trackImpression(v.id);
    });
  }, [adv, trigger]);

  return {
    variant,
    click: (props?) => variant && adv.click(variant.id, props),
    dismiss: () => variant && adv.dismiss(variant.id),
    success: (props?) => variant && adv.success(variant.id, props),
  };
}

Full example: sdks/javascript/examples/react-hook.tsx.

Server-side / Edge

Works anywhere fetch + crypto.subtle exist (Node 18+, Bun, Cloudflare Workers, Vercel Edge).

Request
/* Cloudflare Worker — server-render a personalized hero block */
export default {
  async fetch(request: Request) {
    const adv = new Adverfly({
      workspaceId: 188334,
      manualIdentity: true,  // we manage identity ourselves
    });
    await adv.identify({ customerId: getCookieUserId(request) });

    const variant = await adv.personalize({
      trigger: "ssr_hero",
      context: { country: request.cf?.country },
    });

    return new Response(renderHero(variant?.config), {
      headers: { "content-type": "text/html" },
    });
  },
};

Privacy

  • Emails are hashed client-side (SHA-256, lowercased + trimmed) before any network call.
  • No cookies set by the SDK. Anonymous IDs go to localStorage; opt out with manualIdentity: true.
  • CORS-clean. POST + JSON, no preflight surprises.

See also

  • Widgets — the no-code drop-in pixel option
  • Tracking Events — base event schema (the SDK rides on the same beacon)

Heat Maps & Session Replays

Enable click/scroll capture and session recording in the tracking pixel

The v4 tracking pixel can capture on-page interactions for the Heat Maps app and record full sessions for the Session Replays app. Both are off by default and enabled per site with one flag each in your pixel snippet.

Heat Maps

<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.activate_heatmap = true;
</script>

What gets captured:

  • Clicks — position (relative to document size, so all viewports aggregate correctly), the clicked element's selector, and up to 60 characters of its text.
  • Scroll depth — one max-depth summary per pageview.

Events are batched client-side and sent through the regular beacon endpoint. SPA route changes (pushState / popstate) are handled automatically.

Session Replays

<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.activate_session_replay = true;

  /* Optional tuning */
  window.adverfly.replay_config = {
    sample_rate: 0.25,              /* record 25% of sessions (only when the app has no setting) */
    mask_selectors: [".account-menu"], /* extra elements whose text is masked */
    max_duration_s: 1800,           /* hard stop per session (default 30 min) */
  };
</script>

Sampling without touching your site

You don't need sample_rate in the snippet. The Recording setting in the Session Replays app (100 % down to 1 % of sessions) is read by the pixel directly and reaches new visitor sessions within a few minutes, with no theme change. Without a saved setting 10 % of sessions are recorded — the default takes effect from a site's first recording on. A saved Recording setting always wins over a sample_rate in the snippet; the snippet value only applies while the app has none.

The recorder is built on rrweb — the open-source engine behind most session-replay products. It is lazy-loaded only when recording actually starts, takes a DOM snapshot, then streams mutations, mouse movement, clicks, scrolls, and viewport changes in chunks to api.adverfly.com/replays/v1.

Diagnostics

Alongside the DOM, the recorder captures the three things that explain why a session went wrong. They appear in the player's Console and Network tabs and drive the "Errors" and "Frustration" filters.

CapturedWhat is storedWhat is never stored
JavaScript errorsMessage, error source, stack — each truncated and stripped of query stringsLocal variables, DOM state, anything an error object carries beyond its message
console.error / console.warnUp to four arguments, strings onlyObjects and arrays are recorded as [object], never serialised
Network requestsMethod, URL path, status code, durationRequest bodies, response bodies, headers, cookies, tokens
Page focusThat the visitor switched tab or window, and whenWhich tab, which site, anything about it

Your own console.error calls still run untouched — the original is always invoked first, and a failure inside our hook cannot break your page. Adverfly's own chunk uploads are excluded from the network log.

Page-focus markers exist for one reason: a click that opens a new tab or starts a download changes nothing on the page, and without them it would be misreported as a "dead click".

Privacy guarantees (always on)

  • Input, textarea, and select values are masked in the browser — real values never leave the page. Password and credit-card fields are fully redacted.
  • Elements with class="adv-no-capture" or data-adv-no-capture are dropped entirely from the recording.
  • Elements with class="adv-mask" or data-adv-mask keep their layout but have all text masked.
  • script, iframe, canvas, video, and audio elements are replaced by sized placeholders.
  • If a supported consent tool (Cookiebot, OneTrust, Usercentrics, TCF) reports consent as denied, the recorder does not start.

Recording both apps reuses the same session key, so heatmap data and replays line up per session.

Upsell on your storefront

Render Adverfly upsell prompts on product, cart, checkout and thank-you pages

Upsell rules you activate in the dashboard are published as a public JSON snapshot. Pixel v4 fetches that snapshot once per page, picks the highest-priority rule whose trigger matches the context you give it, renders the prompt, and reports impressions, accepts and declines back to your workspace. Changes go live within about a minute.

The pixel does everything except one step: it cannot add a line item to your cart. That handoff is described in Accepting an offer.

1. Turn the module on

Off by default, so no shop starts showing prompts by accident.

Request
<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", 8397799);
  window.adverfly.activate_upsell = true;
</script>

With this set, the Post-purchase placement renders on its own on any page that already carries a transaction_id — your thank-you page. Every other placement needs step 2, because only your storefront knows what is in the cart.

2. Tell the pixel what the shopper is looking at

Request
advPxl("upsell", "cart", {
  product_ids: ["8412990013", "8412990014"],
  cart_value_minor: 5900,
});
PlacementWhen to call
product_pageon a product detail page, with that product's id
cartwhen the cart or cart drawer renders or changes
checkouton the checkout page
post_purchaseautomatic when transaction_id is set
thank_youorder confirmation, if you want a second prompt

Context fields, all optional:

FieldTypeMeaning
product_idsstring[]what the shopper is viewing or has in the cart
cart_value_minornumbercart subtotal in minor units (cents)
langstringoverrides the detected language for translated copy
containerstring | Elementrender inline into this element instead of as an overlay

By default the prompt renders as an overlay in the position configured on the rule. To place it inline, either pass container, or drop a placeholder in your markup:

Request
<div data-adv-upsell="cart"></div>

Accepting an offer

The pixel hands the offer over and lets your storefront perform the actual cart mutation. Define a handler, or listen for the event:

Request
window.adverfly.upsell_add_to_cart = function (offer, rule) {
  /* offer = { product_id, product_name, price_minor, quantity, discount } */
  return fetch("/cart/add.js", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ id: offer.product_id, quantity: offer.quantity }),
  });
};
Request
document.addEventListener("adverfly:upsell:accept", function (e) {
  console.log(e.detail.offer, e.detail.rule);
});

If neither is present the accept is still tracked, but nothing lands in the cart — so wire one of them up before activating a rule.

Events

Every prompt reports through the normal event pipeline. These are what the Upsell app's take rate and added-value columns are computed from.

EventFires when
upsell_impressionthe prompt is rendered
upsell_clickthe shopper closes it via the ✕
upsell_acceptedthe shopper takes the offer
upsell_declinedthe shopper explicitly declines

Manual control

Request
/* Render a specific placement and get the matched rule back. */
window.advUpsell.render("checkout", { product_ids: ["8412990013"] })
  .then(function (rule) { console.log(rule); });

/* Inspect what is published, without matching. */
window.advUpsell.getRules("cart").then(function (rules) { console.log(rules); });

/* Dismiss whatever is on screen. */
window.advUpsell.close();

Server-rendered storefronts

If you render the prompt yourself — a Shopify theme app extension, headless middleware — read the same published rules over HTTP instead of loading the pixel:

Request
curl "https://api.adverfly.com/upsell/v1/rules?workspace_id=8397799&placement=checkout"

It serves the same snapshot the pixel reads, filtered to rules that are currently within their schedule, so the two paths can never disagree. You then own the matching and the event reporting.

Web Vitals & Error Tracking

Measure what is slow and capture what is broken, from the pixel you already have

Two flags turn the tracking pixel into a thin performance and error monitor. Neither replaces a dedicated monitoring tool, and neither is meant to. What they give you is the thing a separate tool cannot: page speed and JavaScript errors sitting in the same table as revenue, so "the checkout is slow on mobile" and "conversion rate fell on mobile" can be the same query.

Core Web Vitals

<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.activate_web_vitals = true;
</script>

One web_vitals event per pageview, carrying whatever the browser reported:

MetricWhat it measuresGood
lcpLargest Contentful Paint, when the main content appearedunder 2.5 s
clsCumulative Layout Shift, how much the page jumpedunder 0.1
inpInteraction to Next Paint, the worst interaction delayunder 200 ms
fcpFirst Contentful Paint, when anything appearedunder 1.8 s
ttfbTime to First Byte, how long the server tookunder 800 ms

Times are whole milliseconds, cls has three decimals, and pathname is attached so you can group by template rather than by URL.

The numbers come from the browser's own PerformanceObserver, with no library loaded. A browser that does not support an entry type simply reports one metric fewer, so Safari yields fewer fields than Chrome.

Everything is batched and sent once, when the page goes away, over sendBeacon so that leaving does not cancel it. There is deliberately no timer. Two of these metrics are defined over the whole visit: a cookie banner that shifts the layout after six seconds, or a tap twenty seconds in, belongs in the number. Flushing early would quietly report a better score than the visitor experienced, and you would find out when Search Console disagreed.

cls is the worst five-second window of shifts, not the sum over the visit, and inp counts real interactions only, so a slow hover handler is not reported as the responsiveness of someone who never clicked. Both follow Google's definitions, so the figures are comparable with Search Console and PageSpeed Insights.

Measurements above fifteen minutes are dropped. A value that large is a backgrounded tab or a clock jump, not a page that took fifteen minutes to paint, and one such outlier is enough to ruin an average.

What to do with it

Break your pageviews down by pathname and sort by lcp. The slowest template with meaningful traffic is where the money is. Compare mobile against desktop before you compare pages: the gap between the two is usually larger than the gap between your best and worst template.

Error tracking

<script>
  window.adverfly = window.adverfly || [];
  advPxl("init", YOUR_WORKSPACE_ID);
  window.adverfly.activate_error_tracking = true;
</script>

Uncaught errors and unhandled promise rejections arrive as exception events with the type, the message, a truncated stack, the source and the pathname.

Why a shop owner should care, in one sentence: a checkout button that throws on one browser costs orders silently, conversion rate drops, nothing in the analytics says why, and the traffic gets the blame.

What keeps it from becoming a log sink:

  • The same error is sent once per pageview. The fingerprint is type, message and first stack frame, so a handler that throws on every mouse move costs one event rather than ten thousand.
  • At most ten distinct errors per pageview.
  • Cross-origin script errors are dropped. A browser reports them as Script error. with no file, line or stack, which is not actionable. To see them, add crossorigin="anonymous" to your own script tags and serve them with a permissive CORS header.
  • Errors thrown inside the pixel are dropped, judged by the stack and never by the message. ReferenceError: advPxl is not defined thrown by your theme is the one error that proves our script was blocked, so it reaches you.
  • Automated traffic is excluded, like everywhere else.

Reporting an error yourself

A handled error, one you caught and recovered from, is often the more interesting one. Report it deliberately. This needs no flag: the call in your code is the opt-in. It still respects bot filtering and the per-page cap.

try {
  await submitOrder();
} catch (err) {
  advPxl("exception", err);
  showRetry();
}

From the npm package:

import { captureException } from "@adverfly/pixel";
captureException(err);

What is never captured

The message, the stack and the path, and nothing else. Not local variables, not DOM state, not request or response bodies, not anything an error object carries beyond its message. If your error messages contain personal data today, they will travel; strip them at the throw site.

Both together

Neither flag depends on the other, and both are off by default. Session Replay captures errors too, inside the recording, where they explain a single session. These events exist to be aggregated: which template, which browser, how often, and whether it moves with revenue.

Data Quality

How the pixel keeps bots out of your numbers and gets events through an unreliable network

Two things quietly decide whether a dashboard can be trusted: whether the traffic in it is human, and whether the events that were supposed to arrive actually did. Both are handled in the pixel, both are on by default, and neither needs configuration.

Bot filtering

Crawlers that execute JavaScript are indistinguishable from customers unless something says otherwise. They generate pageviews, they get a visitor id, they get bucketed into A/B variants, and they skew every rate that has visitors in its denominator.

This is not hypothetical. On this platform, one "visitor" produced 10,949 of a shop's 11,886 pageviews in a single day, and two other shops carried a crawler that generated up to 46,386 pageviews from one visitor id. A conversion rate computed over that traffic is meaningless.

The pixel therefore decides, before the first request leaves the browser, whether it is talking to a person:

SignalWhat it catches
User agentSearch crawlers, SEO tools, uptime monitors, AI crawlers, HTTP libraries, headless Chrome
navigator.webdriverAny browser under automation control
Injected globalsSelenium, Puppeteer, Playwright, Nightmare and friends, even with a spoofed user agent
Headless shapeZero plugins and no languages, a combination a real browser never reports

A detected bot costs one script evaluation and nothing else. No events, no session, no A/B enrollment, no recording, no heatmap, no replay.

The check is deliberately conservative. A false positive silently deletes a real customer's whole journey, which is worse than letting a crawler through, so every signal has to be something an ordinary browser never reports. Zero plugins alone is not enough, because privacy browsers strip them; it counts only together with an empty language list.

Adding your own

Load testing, a monitoring script or an internal crawler that does not identify itself in the default list:

<script>
  window.adverfly.extra_bot_patterns = ["mycrawler", /loadtest/i];
</script>

Strings are matched case-insensitively against the user agent; regular expressions are used as given.

Turning it off

<script>
  window.adverfly.disable_bot_detection = true;
</script>

Do this only to reproduce a problem. Left on in production it puts crawler traffic back into every rate you report.

Checking a browser

window.adverflyIsBot()   // true when this browser is not being counted

Useful when a test environment reports no data: an automated browser is doing exactly what it is supposed to do, which is nothing.

Delivery, and its one hard limit

A tracking request fails for boring reasons: a tunnel, a flaky connection, a rate limit, a page that unloads mid-request. Every one of those is a conversion missing from a report nobody will question, because a missing row looks like a quiet day.

The obvious answer is to send it again. The reason we do that only in narrow circumstances is worth stating plainly, because it shapes everything below: ingestion does not deduplicate. Each event is stamped and keyed on the server when it arrives, so a second delivery of the same event is a second row, and for a purchase a second sale. Inflated revenue that nobody can explain is far worse than one missing pageview.

So a request is only repeated when the failure proves it was never processed:

FailureRepeated?Why
429, 502, 503, 504Yes, twice, with backoffThe edge turned the request away. It cannot already exist
500NoThe handler may have written the event and then failed
TimeoutNoThe request may have been handled and only the response lost
Network error while onlineNoSame ambiguity
Network error while offlineQueued, if you enabled the queueThe browser reporting itself offline is proof the event did not arrive

Unload-safe sending covers the other common loss: events that happen as the page goes away, the batched Web Vitals among them, go out over sendBeacon, which the browser completes after the page is gone. An ordinary request is cancelled there.

The offline queue

Off by default. Turn it on with one line:

<script>
  window.adverfly.activate_offline_queue = true;
</script>

An event that failed while the browser was offline is then parked in localStorage and replayed on the visitor's next pageview. The queue holds at most 25 events and 64 KB, discards anything older than 24 hours, and drops ordinary events before conversions when it has to make room, so a burst of offline browsing cannot push a purchase out.

It is opt-in because of what a replay costs. A replayed event is stamped with the time it arrives, not the time it happened. A purchase parked at 22:00 and replayed at 09:00 the next morning lands on the wrong day, in a session that has no ad click on it: the revenue moves and the attribution is wrong. That is a different kind of wrong from simply losing the event, so it is your call rather than ours.

Turn it on when recovering the event matters more than the date being exact, which for most shops means: turn it on if you sell to people on trains.

What none of this fixes

Ad blockers and tracking-protection lists block the request before the pixel can do anything about it, and no client-side mechanism changes that. Proxying the bundle and the beacon through your own domain does. It is the single largest recoverable gap in any client-side measurement, typically worth between 10 and 30 percent of events depending on the audience. Talk to support if you want it set up.

Virtual try-on

Optional photo previews, email requests and controlled storefront experiments

Virtual try-on is configured in Collect → Try-on. It edits a visitor's photo using a product reference; it is not a live camera filter. The first recipes are eyelashes and press-on nails.

Enable Pixel v5

After deploying v5, replace the existing v4 script URL with https://sos-de-fra-1.exo.io/adv/advv5.01.js. Keep exactly one pixel script on the page; adding the flag to a v4 installation does not enable Try-on.

Set the flag in your existing Adverfly snippet before Pixel v5 loads:

window.adverfly = window.adverfly || [];
window.adverfly.activate_try_on = true;

The flag alone does not activate the experience. The workspace must also have an enabled configuration, current eligible product coverage and approved photo consent copy. Configure these in the app. The backend must be deployed first.

Place an optional anchor next to your product variants:

<div data-adverfly-tryon></div>

For a theme preview, set window.adverfly.try_on_preview = true only on unpublished themes (for example Shopify.theme.role !== "main"). The button then appears without experiment assignment and no Try-on or exposure events are sent; generations still count as real images.

Alternatively configure a selector in the app. Without an anchor or selector, the module attaches to the product's add-to-cart form. Only eligible product pages show the button. Images are generated after an explicit photo-consent checkbox. Marketing signup is a separate, optional checkbox after the result.

Product setup

Select collection handles, tags or all products, save while disabled, then run Refresh coverage. When collection and tag filters are both set, both must match. Eyelashes use the second Shopify product image. An optional namespace.key metafield overrides the reference; a missing override makes that product ineligible. Inspect references manually before enabling the shop.

Experiments and reporting

Create a draft in Try-on and start it in A/B Testing. The existing storefront is the control; the other group receives Try-on and optional email capture. Assignment is stable per visitor. Exposure is recorded for both groups only on eligible product pages. Purchase conversion uses the existing experiment engine.

The module emits tryon_opened, tryon_consented, tryon_generated, tryon_failed, tryon_switched_style, tryon_add_to_cart and tryon_lead_captured. These events contain no photo, email address or upload URL.

Contacts, limits and rollout

Email requests are listed in the app with their consent version and unconfirmed status. This feature does not send a result email or subscribe contacts to a newsletter automatically. Verify your confirmation integration separately.

The app shows daily, session and monthly limits and your package's image price. There is no monthly base fee: each successful new image costs the per-image price of your package (Enterprise: no Adverfly image fee). Technical failures and downloads do not incur another image charge.

Launch requires customer-approved photo and marketing copy, privacy information, processing-location review and a staged deletion test. Photos and results are private and have a 23-hour access deadline; scheduled deletion runs every 15 minutes, with S3 Lifecycle as a backstop. Closing the dialog requests deletion. Disable the workspace configuration to stop new jobs; cleanup continues.