Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions extensions/web-pixel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> | JsonType[]

register(async (extensionApi) => {
Expand Down Expand Up @@ -72,7 +73,21 @@ register(async (extensionApi) => {
const webPostHogPersistedString = await localStorage.getItem(POSTHOG_KEY);
return webPostHogPersistedString
}
async function resolveDistinctId(): Promise<string> {

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<string> {
if (preferredDistinctId) {
await setDistinctId(preferredDistinctId);
return preferredDistinctId;
}

const webPostHogPersistedString = await getPostHogLocalStorage()
const webPostHogPersisted: {
distinct_id: string;
Expand Down Expand Up @@ -168,15 +183,16 @@ 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',
flushAt: 10,
flushInterval: 100,
bootstrap: {
distinctId: globalDistinctId,
isIdentifiedId: false,
isIdentifiedId: Boolean(initialStorefrontDistinctId),
},
});

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
68 changes: 68 additions & 0 deletions extensions/web-pixel/src/storefront-identity.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> | undefined) {
if (!customer) {
return {};
}

return customer;
}

function getRecord(value: unknown) {
return value && typeof value === 'object'
? (value as Record<string, unknown>)
: undefined;
}

function getRecordString(
record: Record<string, unknown> | undefined,
key: string,
) {
return getNonEmptyString(record?.[key]);
}

function getNonEmptyString(value: unknown) {
return typeof value === 'string' && value.trim().length > 0
? value
: undefined;
}