From 41d2bdf6013473af7e652a74bd572b502eebd4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20von=20K=C3=A4nel?= Date: Tue, 9 Jun 2026 14:31:01 +0200 Subject: [PATCH] fix: merge checkout events onto storefront distinct ids --- extensions/web-pixel/src/index.ts | 48 +++++++++---- .../web-pixel/src/storefront-identity.ts | 68 +++++++++++++++++++ 2 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 extensions/web-pixel/src/storefront-identity.ts diff --git a/extensions/web-pixel/src/index.ts b/extensions/web-pixel/src/index.ts index da03287..d9e94dd 100644 --- a/extensions/web-pixel/src/index.ts +++ b/extensions/web-pixel/src/index.ts @@ -11,6 +11,7 @@ import { getSearchEngine } from './utils'; import { PixieHogPostHog } from './pixiehog-posthog'; import { webPixelToPostHogEcommerceSpecTransformerMap } from './posthog-ecommerce-spec/transformer-map'; import { webPixelToPostHogEcommerceSpecMap } from './posthog-ecommerce-spec/event-map'; +import { getCheckoutPersonProperties, getStorefrontPostHogDistinctId } from './storefront-identity'; type JsonType = string | number | boolean | null | { [key: string]: JsonType } | Array | JsonType[] register(async (extensionApi) => { @@ -72,7 +73,21 @@ register(async (extensionApi) => { const webPostHogPersistedString = await localStorage.getItem(POSTHOG_KEY); return webPostHogPersistedString } - async function resolveDistinctId(): Promise { + + const setDistinctId = async (str: string) => { + const webPostHogPersistedString = await getPostHogLocalStorage() + const webPostHogPersisted: { + distinct_id: string; + } | null = webPostHogPersistedString ? JSON.parse(webPostHogPersistedString) : {}; + await localStorage.setItem(POSTHOG_KEY, JSON.stringify({...webPostHogPersisted, distinct_id: str })); + } + + async function resolveDistinctId(preferredDistinctId?: string): Promise { + if (preferredDistinctId) { + await setDistinctId(preferredDistinctId); + return preferredDistinctId; + } + const webPostHogPersistedString = await getPostHogLocalStorage() const webPostHogPersisted: { distinct_id: string; @@ -168,7 +183,8 @@ register(async (extensionApi) => { await localStorage.setItem(POSTHOG_KEY, JSON.stringify({ distinct_id })); } - const globalDistinctId = await resolveDistinctId() + const initialStorefrontDistinctId = getStorefrontPostHogDistinctId(init.data.cart) + const globalDistinctId = await resolveDistinctId(initialStorefrontDistinctId) const posthog = new PixieHogPostHog(posthog_api_key, { host: posthog_api_host, persistence: 'memory', @@ -176,7 +192,7 @@ register(async (extensionApi) => { flushInterval: 100, bootstrap: { distinctId: globalDistinctId, - isIdentifiedId: false, + isIdentifiedId: Boolean(initialStorefrontDistinctId), }, }); @@ -325,15 +341,14 @@ register(async (extensionApi) => { ...lastTouchCampaignParams, } as const; - const setDistinctId = async (str: string) => { - const webPostHogPersistedString = await getPostHogLocalStorage() - const webPostHogPersisted: { - distinct_id: string; - } | null = webPostHogPersistedString ? JSON.parse(webPostHogPersistedString) : {}; - await localStorage.setItem(POSTHOG_KEY, JSON.stringify({...webPostHogPersisted, distinct_id: str })); - } + const initialPersonProperties = + init.data.customer?.email + ? { ...init.data.customer, email: init.data.customer.email } + : undefined - if (init.data.customer?.email && anonymous == false && globalDistinctId != init.data.customer.email) { + if (initialStorefrontDistinctId && initialPersonProperties && anonymous == false) { + await posthog.identify(initialStorefrontDistinctId, initialPersonProperties) + } else if (init.data.customer?.email && anonymous == false && globalDistinctId != init.data.customer.email) { await setDistinctId(init.data.customer?.email) await posthog.identify(init.data.customer?.email) } @@ -378,9 +393,16 @@ register(async (extensionApi) => { analytics.subscribe( key, preprocessEvent(async (event, uuid, anonymous) => { - const distinctId = await resolveDistinctId(); + const checkoutStorefrontDistinctId = getStorefrontPostHogDistinctId(event.data.checkout) + const distinctId = await resolveDistinctId(checkoutStorefrontDistinctId); const {sessionId,windowId} = await resolveSessionId(); const eventName = resolveEventEcommerceName(event.name); + const checkoutPersonProperties = getCheckoutPersonProperties(event.data.checkout) + + if (checkoutStorefrontDistinctId && anonymous == false) { + await posthog.identify(checkoutStorefrontDistinctId, checkoutPersonProperties) + } + await posthog.captureStatelessPublic(distinctId, eventName, { ...featureFlags, ...initProperties, @@ -418,7 +440,7 @@ register(async (extensionApi) => { }); const email = event.data.checkout.email - if (email && anonymous == false && distinctId != email) { + if (!checkoutStorefrontDistinctId && email && anonymous == false && distinctId != email) { await setDistinctId(email) await posthog.identify(email) } diff --git a/extensions/web-pixel/src/storefront-identity.ts b/extensions/web-pixel/src/storefront-identity.ts new file mode 100644 index 0000000..ef900fd --- /dev/null +++ b/extensions/web-pixel/src/storefront-identity.ts @@ -0,0 +1,68 @@ +import type { Attribute } from '@shopify/web-pixels-extension'; + +const STOREFRONT_POSTHOG_DISTINCT_ID_ATTRIBUTE = '_posthog_distinct_id'; + +type CheckoutLike = { + attributes?: Attribute[] | null; + customer?: unknown; + email?: string | null; + order?: unknown; +}; + +export function getStorefrontPostHogDistinctId(checkout?: CheckoutLike | null) { + let attribute = checkout?.attributes?.find( + ({ key }) => key === STOREFRONT_POSTHOG_DISTINCT_ID_ATTRIBUTE, + ); + return getNonEmptyString(attribute?.value); +} + +export function getCheckoutPersonProperties(checkout?: CheckoutLike | null) { + let customer = getCheckoutCustomer(checkout); + let email = getNonEmptyString( + checkout?.email ?? getRecordString(customer, 'email'), + ); + if (!email) { + return undefined; + } + + return { + ...getCustomerProperties(customer), + email, + }; +} + +function getCheckoutCustomer(checkout?: CheckoutLike | null) { + let directCustomer = getRecord(checkout?.customer); + if (directCustomer) { + return directCustomer; + } + + return getRecord(getRecord(checkout?.order)?.customer); +} + +function getCustomerProperties(customer: Record | undefined) { + if (!customer) { + return {}; + } + + return customer; +} + +function getRecord(value: unknown) { + return value && typeof value === 'object' + ? (value as Record) + : undefined; +} + +function getRecordString( + record: Record | undefined, + key: string, +) { + return getNonEmptyString(record?.[key]); +} + +function getNonEmptyString(value: unknown) { + return typeof value === 'string' && value.trim().length > 0 + ? value + : undefined; +}