Skip to content

Analytics & Event Tracking

Feature Type

PWA Editor Feature - Google Tag Manager (GTM) / GA4 event tracking, and how it behaves across webview, iframe, and mobile deployments.

The PWA Editor sends product-analytics events (page views, ecommerce events, in-editor engagement) to Google Analytics 4. This guide covers what's sent by default, how to receive it in your own GTM/GA4 setup, and - the part that trips most integrations up - how user identity does or doesn't carry across the boundary between your application and the embedded editor.

Two Separate Questions

Everything below answers one of two independent questions. Keep them apart - they don't affect each other.

QuestionWhat answers it
Routing - which GA4 property receives a given eventYour GA4 Measurement ID, on file with PBAI (see Receiving Our Events)
Identity - which user a given event is attributed toCookie sharing or manual linking, depending on your deployment (see Cross-Frame Identity)

Receiving Our Events

Default Behavior

Once your GA4 Measurement ID is on file with PBAI, standard editor and ecommerce events are already routed to your property. Nothing below is required just to receive events - it's what you need if you want to build your own GTM tags/triggers around them (in your own GTM container, if you embed one - see Embedding Your Own GTM Container), or explore them meaningfully in GA4.

Event & Field Reference

Internal Checkout Only (Flow A)

The ecommerce events below (begin_checkout through purchase) only fire when your integration uses Flow A: Internal Checkout (see Channel Messaging). If you use Flow B: External Checkout, none of these fire - checkout happens entirely on your own systems, outside the editor. See Server-Side Order Tracking for the equivalent signal in that flow.

ℹ️

This list is continuously updated, and new items will be added over time.

EventFires whenKey fields
spa_page_viewUser navigates between editor routespage_path
image_uploadUser selects photos to upload (an attempt — fires when files are chosen, not when the upload succeeds)upload_source, upload_count
upload_picker_openedUser taps an upload method and the photo picker is about to openupload_source, upload_method
upload_picker_dismissedPhoto picker closed without returning photos (user cancelled, or iOS failed to export them)upload_source, upload_method
upload_errorAn upload attempt could not startupload_source, upload_error_reason, upload_method
template_changeUser swaps the project's templatefrom_template, to_template
relayoutUser relays out a pagerelayout_page_number, relayout_total_page_count, relayout_page_photo_count
pagebuildInitial page build completespagebuild_curate, pagebuild_photos_available, pagebuild_photos_used
density_changeUser changes page densitydensity_type, density_page_count
not_foundA route errors as a 404 (not fired for a plain URL navigation to a not-found path, since that's already captured by page-view tracking)page_path
back_gestureUser presses Back or uses the iOS edge-swipe on the upload or editor page, and is asked to confirm rather than leaving the projectback_from_route, back_view, back_action
upload_abandonedUser leaves or backgrounds the upload page while photos are still uploading (best effort — some departures cannot be captured, so this undercounts)upload_abandon_phase, upload_abandon_selected, upload_abandon_analyzed, upload_abandon_signed, upload_abandon_how
begin_checkoutUser starts checkout (Flow A only)currency, value, items
add_shipping_infoShipping method selected (Flow A only)currency, value, shipping_tier, items
add_payment_infoPayment method selected (Flow A only)currency, value, payment_type, items
purchaseOrder completes (Flow A only)transaction_id, currency, value, items

Custom Dimensions

Register a custom dimension (Admin → Custom definitions → Create custom dimensions, scope Event) on your GA4 property for any field above you want to explore or filter by - registration is a one-time step per property and doesn't require the event to have fired first.

Numeric Fields Are Metrics, Not Dimensions

Fields holding a count - upload_count, upload_abandon_selected, upload_abandon_analyzed, relayout_*, pagebuild_photos_*, density_page_count, order_project_count - belong under Create custom metrics, a separate tab in the same screen. Registered as a dimension instead, 99 becomes a text value and you get one row per distinct number rather than a total you can average. A registered definition can't be repointed at a different field afterwards, so a mistake here means retiring the name and starting over.

Exploring These Events in GA4

Registration makes a field available; Explorations are where it becomes an answer. This section walks through the setups for the two exit-signal events, back_gesture and upload_abandoned, which is where most people get stuck - they describe why someone left, so they behave differently from the progression events around them.

Not Retroactive

An exploration only sees data from the moment each definition was registered. Set the date range to start there, not to a rolling "last 28 days" - otherwise the days before registration read as zero rather than as "not collected."

Before Building Anything

Fire one real event and confirm it arrives, using GA4 DebugView with GTM Preview on, or DevTools → Network filtered to collect. An exploration built against data that never landed looks identical to a misconfigured one, and you'll spend the time debugging the wrong half.

Free-form: Upload Abandonment

Explore → Blank, then:

WhereSet
Variables → Dimensionsupload_abandon_phase, upload_abandon_signed, upload_abandon_how, Device category
Variables → MetricsEvent count, Total users, upload_abandon_selected, upload_abandon_analyzed
Tab settings → TechniqueFree form
Tab settings → FiltersEvent name exactly matches upload_abandoned
Tab settings → Rowsupload_abandon_phase, then upload_abandon_signed, Nested rows on
Tab settings → ColumnsDevice category
Tab settings → ValuesEvent count, Total users, and the two count metrics

Apply The Event Filter First

Without it, every row is diluted by all other events, where these dimensions are simply (not set). This is the single most common reason an exploration looks broken.

Reading it. upload_abandon_signed distinguishes the two failure modes: false means signing never completed, so nothing was ever sent; true means the upload started and stalled partway. Crossed with upload_abandon_phase, the cell to watch is generating_fastbook × false - a long, silent wait with nothing happening behind it.

Custom metrics aggregate as sums in Explorations, so upload_abandon_selected reads as a total across events rather than an average. Divide by Event count, or build a Calculated metric under Admin → Custom definitions.

Free-form: Back Gesture

Same shape, different fields:

WhereSet
Variables → Dimensionsback_from_route, back_view, back_action, Device category
Variables → MetricsEvent count, Total users
Tab settings → FiltersEvent name exactly matches back_gesture
Tab settings → Rowsback_from_route, then back_view, Nested rows on
Tab settings → Columnsback_action

Reading it. Event count ÷ Total users is the number that matters. A user pressing Back repeatedly is arguing with the interface, not leaving it - a high ratio on a given route says the confirmation isn't answering the question they're asking.

Segments, And Why These Events Aren't Funnel Steps

Neither event belongs as a step in a conversion funnel. A step measures progress toward a goal; these measure departure from it. Inserting one into a sequence fragments the counts and makes the step's completion rate meaningless.

Use a segment instead, applied to the funnel you already have:

  1. In any exploration, Variables column → Segments+.
  2. Choose Session segment. (Scope matters: session answers "what else happened in that visit"; user answers "did that person ever come back". See below.)
  3. Name it, e.g. Sessions with upload abandonment.
  4. Add new condition → search for and select the event upload_abandoned.
  5. Optionally narrow it with a parameter - e.g. upload_abandon_phase exactly matches generating_fastbook - to segment one failure mode rather than all of them.
  6. Save and apply.
  7. Open your funnel exploration → Tab settings → Segment comparisons → drag the new segment in, keeping All Users alongside it. Applied alone, the funnel simply restricts to those sessions and there is nothing to compare against.

Switch the scope to User segment to ask the follow-up question instead: do people who hit this ever return and complete an order? That's a different and often more actionable answer than the within-session one.

Path Exploration: Useful For One, Not The Other

back_gesture as a starting point - worth building. It answers what a Free-form structurally cannot: after the confirmation was raised, did the user stay? Explore → Path exploration → set the start point to the back_gesture event, and set the node type on following steps to Event name rather than the default page/screen, since these all occur within a single route. If the next node is dominated by ordinary engagement events, the confirmation is doing its job; if it's session end, it only delayed the exit.

upload_abandoned as a starting point - don't. It fires as the page is going away, so there is no "after" to trace. The path renders empty and reads as a broken report.

As an ending point it technically works, but the preceding steps are almost always the known upload sequence, so it rarely earns its place.

These Are Shapes, Not Totals

upload_abandoned is best-effort: browsers do not guarantee that a tag fires while a page is unloading, so it undercounts by an unknown margin. back_gesture counts only intercepted Back presses, not every Back press. Read both for distribution - which phase, which device, which route - and never as a complete count of how often something happened.

Embedding Your Own GTM Container

If you want full independent control over these events - your own non-GA4 tags (remarketing pixels, etc.), or you'd simply rather manage your own GTM workspace than depend on PBAI's default routing - PBAI can load your GTM container directly alongside its own inside the editor.

Requesting This

Coordinate with the PBAI team and provide your GTM Container ID. Once enabled, your container runs in the same document as the editor, sharing the same dataLayer - every event in the reference table above is visible to it automatically, no code changes needed per event.

To consume an event in your own container, either:

  • Build a Custom Event trigger + GA4 Event tag per event you care about, mapping fields via dot-notation Data Layer Variables (e.g. payload.upload_count) - ecommerce events (begin_checkout through purchase) instead use the Send Ecommerce data checkbox on the GA4 Event tag, which reads currency/value/items automatically with no manual field mapping, or
  • Build a generic forwarder tag that reads any event's payload object and forwards it to GA4 as-is, if you'd rather not maintain a trigger/tag pair per event.

Avoid Double-Counting

Once your own container's tags are confirmed working, tell your account manager so PBAI can stop its own default routing to your property. Otherwise the same event lands twice - once from PBAI's routing, once from your own container.

Cross-Frame Identity

This section is about identity (which user an event is attributed to), not routing. It only matters if you care about session-level continuity between your own site and the embedded editor - e.g. connecting "a user who was reading a product page on my site" to "the events that happened inside the editor" as one continuous session in your analytics. If you don't need that - event counts and revenue are unaffected either way - you can skip this section.

Why It's Not Automatic

A GA4 property identifies users via a client_id, stored in a cookie (_ga). Cookies are scoped per domain, not per GTM container or per GA4 property - so whether identity carries across a boundary depends entirely on the domain relationship between your site and wherever the editor is running, not on any GTM configuration by itself.

                    Your site
                   (client.com)
                        |
          -------------------------------
          |                             |
     Editor (iframe/webview)      Checkout (external only)
          |                             |
   domain relationship            domain relationship
   to your site determines        to your site determines
   whether identity carries       whether identity carries

Two Independent Hops

When your integration uses external checkout, the checkout hop branches directly off your site, not through the editor. Bridging identity for the editor doesn't automatically bridge it for checkout too - they're evaluated separately.

Same Domain Family (subdomain of your own domain)

If the editor is hosted on a subdomain you own (e.g. app.your-domain.com, embedded in your-domain.com), identity carries automatically. GA4's default cookie behavior scopes _ga to the broadest domain it's permitted to use - from a subdomain, that's your shared registrable domain, the same cookie your own site's session already uses. Nothing to configure.

Requesting This

This requires PBAI to serve your deployment from a subdomain you control. Coordinate with your account manager - it's an infrastructure/DNS step (a CNAME to PBAI), not a dashboard setting.

Different Domain (default deployment)

If the editor stays on PBAI's own domain, there's no native cookie sharing - the two sessions are independent by default. To bridge them, your site needs to pass its own linked identity into the editor's URL when constructing the iframe/webview:

javascript
// before setting the iframe/webview src:
gtag('get', YOUR_GA4_MEASUREMENT_ID, 'linker_param', (param) => {
  iframeEl.src = editorUrl + (editorUrl.includes('?') ? '&' : '?') + param
})

This requires Cross Domain Linking enabled on your own GA4 configuration tag (with PBAI's domain listed), and the same enabled on PBAI's side with your domain listed - coordinate with your account manager to confirm this is configured on PBAI's end. Once linked at load, the identity carries through the whole in-editor session automatically, including checkout if you use Flow A (Internal Checkout).

Verify Before Relying On It

This applies GA4's cross-domain linking to an embed rather than a page redirect - not the textbook use case Google documents it for. Confirm it behaves as expected in GTM Preview / GA4 DebugView before depending on it.

Mobile WebView

A native app WebView isn't the same problem as a browser iframe, even though PBAI's own code treats both as "embedded" for UI purposes. There's no parent web page here - the parent is a native app shell, which has no GTM container and no cookies of its own to share.

  • Cookies do exist inside a WebView (it's a real browser engine), but the cookie jar is sandboxed to your app installation and commonly not persisted across app restarts unless your app explicitly configures that - relying on _ga continuity here is unreliable regardless of any domain question.
  • Cross-domain linking (above) doesn't apply either - there's no second web page generating or receiving a linker param.
  • The practical fix: skip cookies entirely. Have your app hold a stable identifier (your own persisted install ID, or a Firebase App Instance ID if you already run Firebase Analytics), and append it to the editor URL as a nativeClientId query parameter when loading it:
https://{frontend}.photobook.ai/editor?id=p-xyz789&nativeClientId=YOUR_STABLE_ID

The editor picks this up automatically and uses it in place of a cookie-generated GA4 client_id - no other setup needed on your end for this specific piece. Omit the parameter entirely for ordinary sessions; normal cookie-based behavior applies whenever it's absent.

Server-Side Order Tracking

For Flow B: External Checkout integrations, none of the ecommerce events above ever fire, since checkout happens entirely on your own systems. If you want a signal from PBAI's side that an order was actually submitted for production - independent of your own checkout instrumentation - PBAI can send print and purchase events server-side (via the GA4 Measurement Protocol, not GTM) when you call the Print / Complete Job API.

Requesting This

Provide your GA4 property's Measurement ID and API Secret (GA4 Admin → Data Streams → choose your stream → Measurement Protocol API secrets) to your account manager.

EventFires onKey fields
printEvery order, regardless of checkout channelcheckout_source, order_project_count
purchaseExternal checkout orders onlycheckout_source, transaction_id, value, items

purchase is only sent here for external checkout - if you use Flow A (Internal Checkout), the client-side purchase event above already covers it, and sending it again here would double-count revenue.

Truth Table

HostingCheckoutDeploymentStatusRecommended steps
n/an/aStandalone (no parent site)AutomaticNothing needed - one continuous session by construction
Same domain familyFlow A (Internal)iframe/webviewAutomaticNothing needed - cookie_domain: auto covers the whole session
Same domain familyFlow B, checkout same domain as your siteiframe/webviewAutomaticNothing needed - both hops automatic
Same domain familyFlow B, checkout on a different domainiframe/webviewBridge neededEditor hop automatic. Checkout hop needs standard cross-domain linking between your site and checkout domain
Different domainFlow A (Internal)iframe/webviewOptionalAccept the gap, or bridge with the linker param passed into the editor URL at load
Different domainFlow B, checkout same domain as your siteiframe/webviewBridge needed for editor hopEditor hop: linker param into URL. Checkout hop: automatic (same-origin navigation)
Different domainFlow B, checkout on a different domainiframe/webviewBridge needed for both hopsEditor hop: linker param into URL. Checkout hop: standard cross-domain linking, configured separately
n/aEitherMobile WebViewBridge neededSkip cookies - inject a stable ID from your app and set it as GA4 client_id explicitly

Worked Example

Scenario: Your GTM container embedded in the editor, deployed on a subdomain you own, using your own (external) checkout.

  1. Confirm the subdomain hosting is live - this is what makes identity continuity automatic; verify with your account manager rather than assume it.
  2. Reconsider whether you need the embed at all for GA4 purposes - on a shared domain family, PBAI's default routing to your property already carries correctly-linked identity with no embed required. Embed your own container specifically if you want independent tags (non-GA4 pixels) or your own workspace - not solely to "see" GA4 events, which you'd already get.
  3. Build triggers/tags in your container for the events in the reference table - only the in-editor engagement events apply here, since Flow B never fires the ecommerce events.
  4. Register custom dimensions on your GA4 property for whichever fields you use.
  5. Don't mark engagement events as key events - they're not conversions. For an actual order signal, see Server-Side Order Tracking instead.
  6. Tell your account manager once your tags are verified working, so PBAI can stop its default routing to your property and avoid double-counting.

Common Issues

Events Not Appearing for a Client Property

Confirm the client's GA4 Measurement ID is on file with PBAI, and check the raw network request (DevTools → Network → filter collect) rather than relying solely on GA4 DebugView/Realtime, which can lag for brand-new parameter names.

Session Data Looks Disconnected From My Site

This is expected on a different domain without the linker param bridge configured - see Cross-Frame Identity. Event counts and revenue are unaffected; only session/user-level metrics are.

Same Event Appears Twice

Usually means both PBAI's default routing and your own embedded container are sending the same event to the same property - see the warning in Embedding Your Own GTM Container.

photobook.ai Developer Documentation