@shware/analytics 8.7.0 → 8.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ const keys = {
5
5
  visitor_id: "visitor_id",
6
6
  first_open_time: "first_open_time",
7
7
  first_visit_time: "first_visit_time",
8
+ /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */
9
+ install_referrer_claim_time: "install_referrer_claim_time",
8
10
  session: "session"
9
11
  };
10
12
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"storage.cjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n session: 'session',\n} as const;\n"],"mappings":";;AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;CAClB,SAAS;AACX"}
1
+ {"version":3,"file":"storage.cjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */\n install_referrer_claim_time: 'install_referrer_claim_time',\n session: 'session',\n} as const;\n"],"mappings":";;AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;;CAElB,6BAA6B;CAC7B,SAAS;AACX"}
@@ -4,6 +4,8 @@ declare const keys: {
4
4
  readonly visitor_id: 'visitor_id';
5
5
  readonly first_open_time: 'first_open_time';
6
6
  readonly first_visit_time: 'first_visit_time';
7
+ /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */
8
+ readonly install_referrer_claim_time: 'install_referrer_claim_time';
7
9
  readonly session: 'session';
8
10
  };
9
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.cts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;WACA"}
1
+ {"version":3,"file":"storage.d.cts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;;WAEA;WACA"}
@@ -4,6 +4,8 @@ declare const keys: {
4
4
  readonly visitor_id: 'visitor_id';
5
5
  readonly first_open_time: 'first_open_time';
6
6
  readonly first_visit_time: 'first_visit_time';
7
+ /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */
8
+ readonly install_referrer_claim_time: 'install_referrer_claim_time';
7
9
  readonly session: 'session';
8
10
  };
9
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.mts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;WACA"}
1
+ {"version":3,"file":"storage.d.mts","names":[],"sources":["../../src/constants/storage.ts"],"mappings":";cAAa;WACX;WACA;WACA;WACA;;WAEA;WACA"}
@@ -4,6 +4,8 @@ const keys = {
4
4
  visitor_id: "visitor_id",
5
5
  first_open_time: "first_open_time",
6
6
  first_visit_time: "first_visit_time",
7
+ /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */
8
+ install_referrer_claim_time: "install_referrer_claim_time",
7
9
  session: "session"
8
10
  };
9
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"storage.mjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n session: 'session',\n} as const;\n"],"mappings":";AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;CAClB,SAAS;AACX"}
1
+ {"version":3,"file":"storage.mjs","names":[],"sources":["../../src/constants/storage.ts"],"sourcesContent":["export const keys = {\n device_id: 'device_id',\n visitor_id: 'visitor_id',\n first_open_time: 'first_open_time',\n first_visit_time: 'first_visit_time',\n /** Native: when a launch took the install referrer's utm as its own (see native `getTags`). */\n install_referrer_claim_time: 'install_referrer_claim_time',\n session: 'session',\n} as const;\n"],"mappings":";AAAA,MAAa,OAAO;CAClB,WAAW;CACX,YAAY;CACZ,iBAAiB;CACjB,kBAAkB;;CAElB,6BAA6B;CAC7B,SAAS;AACX"}
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_setup_index = require("../setup/index.cjs");
3
+ const require_constants_storage = require("../constants/storage.cjs");
3
4
  let react_native = require("react-native");
4
5
  let expo_application = require("expo-application");
5
6
  let expo_device = require("expo-device");
@@ -72,12 +73,38 @@ function getDeviceType() {
72
73
  default: return;
73
74
  }
74
75
  }
76
+ /**
77
+ * The install referrer's utm belongs to one launch: the first one that has the referrer in hand
78
+ * — normally the install launch, since `first_open` is the first event the app tracks and
79
+ * tracking it builds the first tags. That launch claims the referrer by writing a marker of its
80
+ * own, and holds the claim for the rest of the process, so every event of the launch and the
81
+ * visitor created from it carry the utm; every later launch finds the marker and sends the raw
82
+ * `install_referrer` alone. Cleared storage and a reinstall lose the marker and claim again,
83
+ * exactly as they make `first_open` fire again.
84
+ *
85
+ * Claimed only once a referrer has actually been resolved. The Install Referrer API needs the
86
+ * Play Store service, which is at its least available right after an install; a launch that
87
+ * could not reach it must not spend the claim on nothing, because the referrer stays readable
88
+ * for 90 days and a later launch can still take it — late attribution, rather than none. iOS
89
+ * has no referrer and so never claims.
90
+ *
91
+ * A marker of its own rather than `first_open_time`, so that nothing here depends on when the
92
+ * hook writes that one relative to the first `getTags`. The storage read waits for that call:
93
+ * `setupAnalytics` and this module's evaluation can run where no storage exists.
94
+ */
95
+ let installLaunch;
96
+ function claimInstallReferrer() {
97
+ if (require_setup_index.config.storage.getItem(require_constants_storage.keys.install_referrer_claim_time)) return false;
98
+ require_setup_index.config.storage.setItem(require_constants_storage.keys.install_referrer_claim_time, (/* @__PURE__ */ new Date()).toISOString());
99
+ return true;
100
+ }
75
101
  async function getTags() {
76
102
  const screen = react_native.Dimensions.get("screen");
77
103
  const screen_width = Math.floor(screen.width);
78
104
  const screen_height = Math.floor(screen.height);
79
105
  const install_referrer = await getInstallReferrer();
80
- const params = new react_native_url_polyfill.URLSearchParams(install_referrer);
106
+ if (install_referrer && installLaunch === void 0) installLaunch = claimInstallReferrer();
107
+ const params = new react_native_url_polyfill.URLSearchParams(installLaunch ? install_referrer : void 0);
81
108
  const tags = {
82
109
  os: `${expo_device.osName} ${expo_device.osVersion}`,
83
110
  os_name: expo_device.osName ?? void 0,
@@ -1 +1 @@
1
- {"version":3,"file":"setup.cjs","names":["Platform","getInstallReferrerAsync","getIosIdForVendorAsync","getAndroidId","randomUUID","deviceType","DeviceType","Dimensions","URLSearchParams","osName","osVersion","modelName","manufacturer","modelId","PixelRatio","config","getLocales","getCalendars","getAdvertisingId"],"sources":["../../src/native/setup.ts"],"sourcesContent":["import 'expo-sqlite/localStorage/install';\nimport { getAndroidId, getInstallReferrerAsync, getIosIdForVendorAsync } from 'expo-application';\nimport { randomUUID } from 'expo-crypto';\nimport {\n DeviceType,\n deviceType,\n manufacturer,\n modelId,\n modelName,\n osName,\n osVersion,\n} from 'expo-device';\nimport { getCalendars, getLocales } from 'expo-localization';\nimport { getAdvertisingId } from 'expo-tracking-transparency';\nimport { Dimensions, PixelRatio, Platform } from 'react-native';\nimport { URLSearchParams } from 'react-native-url-polyfill';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n\n/**\n * `getTags` runs once per event now, and both of these reach for a native module. Neither answer\n * can change while the app is running, so each is resolved once and the promise is reused. A\n * failed lookup is not cached, so the next event tries again.\n */\nlet deviceIdPromise: Promise<string> | undefined;\nlet installReferrerPromise: Promise<string | undefined> | undefined;\n\nexport function getDeviceId(): Promise<string> {\n deviceIdPromise ??= resolveDeviceId().catch((error: unknown) => {\n deviceIdPromise = undefined;\n throw error;\n });\n return deviceIdPromise;\n}\n\nfunction getInstallReferrer(): Promise<string | undefined> {\n if (Platform.OS !== 'android') return Promise.resolve(undefined);\n installReferrerPromise ??= getInstallReferrerAsync().catch((error: unknown) => {\n installReferrerPromise = undefined;\n throw error;\n });\n return installReferrerPromise;\n}\n\nasync function resolveDeviceId(): Promise<string> {\n let deviceId: string | null = null;\n if (Platform.OS === 'ios') {\n deviceId = await getIosIdForVendorAsync();\n } else if (Platform.OS === 'android') {\n deviceId = getAndroidId();\n }\n if (!deviceId) {\n deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = randomUUID();\n localStorage.setItem('device_id', deviceId);\n }\n }\n return deviceId;\n}\n\nexport function getDeviceType(): string | undefined {\n switch (deviceType) {\n case DeviceType.PHONE:\n return 'mobile';\n case DeviceType.TABLET:\n return 'tablet';\n case DeviceType.DESKTOP:\n return 'desktop';\n case DeviceType.TV:\n return 'smarttv';\n default:\n return undefined;\n }\n}\n\nexport async function getTags(): Promise<TrackTags> {\n const screen = Dimensions.get('screen');\n const screen_width = Math.floor(screen.width);\n const screen_height = Math.floor(screen.height);\n\n const install_referrer = await getInstallReferrer();\n const params = new URLSearchParams(install_referrer);\n\n const tags: TrackTags = {\n os: `${osName} ${osVersion}`,\n os_name: osName ?? undefined,\n os_version: osVersion ?? undefined,\n device: modelName ?? undefined,\n device_id: await getDeviceId(),\n device_type: getDeviceType(),\n device_vendor: manufacturer ?? undefined,\n device_model_id: modelId ?? undefined,\n device_pixel_ratio: PixelRatio.get(),\n screen_width,\n screen_height,\n screen_resolution: `${screen_width}x${screen_height}`,\n release: config.release,\n language: getLocales().at(0)?.languageTag ?? 'en',\n time_zone: getCalendars().at(0)?.timeZone ?? 'UTC',\n // ads\n advertising_id: getAdvertisingId() ?? undefined,\n install_referrer,\n // utm params\n utm_source: params.get('utm_source') ?? undefined,\n utm_medium: params.get('utm_medium') ?? undefined,\n utm_campaign: params.get('utm_campaign') ?? undefined,\n utm_term: params.get('utm_term') ?? undefined,\n utm_content: params.get('utm_content') ?? undefined,\n utm_id: params.get('utm_id') ?? undefined,\n utm_source_platform: params.get('utm_source_platform') ?? undefined,\n utm_creative_format: params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic: params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"setup.cjs","names":["Platform","getInstallReferrerAsync","getIosIdForVendorAsync","getAndroidId","randomUUID","deviceType","DeviceType","config","keys","Dimensions","URLSearchParams","osName","osVersion","modelName","manufacturer","modelId","PixelRatio","getLocales","getCalendars","getAdvertisingId"],"sources":["../../src/native/setup.ts"],"sourcesContent":["import 'expo-sqlite/localStorage/install';\nimport { getAndroidId, getInstallReferrerAsync, getIosIdForVendorAsync } from 'expo-application';\nimport { randomUUID } from 'expo-crypto';\nimport {\n DeviceType,\n deviceType,\n manufacturer,\n modelId,\n modelName,\n osName,\n osVersion,\n} from 'expo-device';\nimport { getCalendars, getLocales } from 'expo-localization';\nimport { getAdvertisingId } from 'expo-tracking-transparency';\nimport { Dimensions, PixelRatio, Platform } from 'react-native';\nimport { URLSearchParams } from 'react-native-url-polyfill';\nimport { keys } from '../constants/storage';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n\n/**\n * `getTags` runs once per event now, and both of these reach for a native module. Neither answer\n * can change while the app is running, so each is resolved once and the promise is reused. A\n * failed lookup is not cached, so the next event tries again.\n */\nlet deviceIdPromise: Promise<string> | undefined;\nlet installReferrerPromise: Promise<string | undefined> | undefined;\n\nexport function getDeviceId(): Promise<string> {\n deviceIdPromise ??= resolveDeviceId().catch((error: unknown) => {\n deviceIdPromise = undefined;\n throw error;\n });\n return deviceIdPromise;\n}\n\nfunction getInstallReferrer(): Promise<string | undefined> {\n if (Platform.OS !== 'android') return Promise.resolve(undefined);\n installReferrerPromise ??= getInstallReferrerAsync().catch((error: unknown) => {\n installReferrerPromise = undefined;\n throw error;\n });\n return installReferrerPromise;\n}\n\nasync function resolveDeviceId(): Promise<string> {\n let deviceId: string | null = null;\n if (Platform.OS === 'ios') {\n deviceId = await getIosIdForVendorAsync();\n } else if (Platform.OS === 'android') {\n deviceId = getAndroidId();\n }\n if (!deviceId) {\n deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = randomUUID();\n localStorage.setItem('device_id', deviceId);\n }\n }\n return deviceId;\n}\n\nexport function getDeviceType(): string | undefined {\n switch (deviceType) {\n case DeviceType.PHONE:\n return 'mobile';\n case DeviceType.TABLET:\n return 'tablet';\n case DeviceType.DESKTOP:\n return 'desktop';\n case DeviceType.TV:\n return 'smarttv';\n default:\n return undefined;\n }\n}\n\n/**\n * The install referrer's utm belongs to one launch: the first one that has the referrer in hand\n * — normally the install launch, since `first_open` is the first event the app tracks and\n * tracking it builds the first tags. That launch claims the referrer by writing a marker of its\n * own, and holds the claim for the rest of the process, so every event of the launch and the\n * visitor created from it carry the utm; every later launch finds the marker and sends the raw\n * `install_referrer` alone. Cleared storage and a reinstall lose the marker and claim again,\n * exactly as they make `first_open` fire again.\n *\n * Claimed only once a referrer has actually been resolved. The Install Referrer API needs the\n * Play Store service, which is at its least available right after an install; a launch that\n * could not reach it must not spend the claim on nothing, because the referrer stays readable\n * for 90 days and a later launch can still take it — late attribution, rather than none. iOS\n * has no referrer and so never claims.\n *\n * A marker of its own rather than `first_open_time`, so that nothing here depends on when the\n * hook writes that one relative to the first `getTags`. The storage read waits for that call:\n * `setupAnalytics` and this module's evaluation can run where no storage exists.\n */\nlet installLaunch: boolean | undefined;\n\nfunction claimInstallReferrer(): boolean {\n if (config.storage.getItem(keys.install_referrer_claim_time)) return false;\n config.storage.setItem(keys.install_referrer_claim_time, new Date().toISOString());\n return true;\n}\n\nexport async function getTags(): Promise<TrackTags> {\n const screen = Dimensions.get('screen');\n const screen_width = Math.floor(screen.width);\n const screen_height = Math.floor(screen.height);\n\n // The install referrer never changes, and it used to be spread into `utm_*` on every launch —\n // so every session of an Android install reported the install campaign as its own acquisition\n // for as long as the app stayed installed, and a report reading a session's tags could not tell\n // the install from the thousandth open. The campaign is the install's touch, not every\n // session's.\n const install_referrer = await getInstallReferrer();\n if (install_referrer && installLaunch === undefined) installLaunch = claimInstallReferrer();\n const params = new URLSearchParams(installLaunch ? install_referrer : undefined);\n\n const tags: TrackTags = {\n os: `${osName} ${osVersion}`,\n os_name: osName ?? undefined,\n os_version: osVersion ?? undefined,\n device: modelName ?? undefined,\n device_id: await getDeviceId(),\n device_type: getDeviceType(),\n device_vendor: manufacturer ?? undefined,\n device_model_id: modelId ?? undefined,\n device_pixel_ratio: PixelRatio.get(),\n screen_width,\n screen_height,\n screen_resolution: `${screen_width}x${screen_height}`,\n release: config.release,\n language: getLocales().at(0)?.languageTag ?? 'en',\n time_zone: getCalendars().at(0)?.timeZone ?? 'UTC',\n // ads\n advertising_id: getAdvertisingId() ?? undefined,\n install_referrer,\n // utm params: from the install referrer, on the launch that claimed it\n utm_source: params.get('utm_source') ?? undefined,\n utm_medium: params.get('utm_medium') ?? undefined,\n utm_campaign: params.get('utm_campaign') ?? undefined,\n utm_term: params.get('utm_term') ?? undefined,\n utm_content: params.get('utm_content') ?? undefined,\n utm_id: params.get('utm_id') ?? undefined,\n utm_source_platform: params.get('utm_source_platform') ?? undefined,\n utm_creative_format: params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic: params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.cts","names":[],"sources":["../../src/native/setup.ts"],"mappings":";;;;cAqBa,SAAS;iBA2BN,eAAe;iBAkCf;iBAeM,WAAW,QAAQ"}
1
+ {"version":3,"file":"setup.d.cts","names":[],"sources":["../../src/native/setup.ts"],"mappings":";;;;cAsBa,SAAS;iBA2BN,eAAe;iBAkCf;iBA0CM,WAAW,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.mts","names":[],"sources":["../../src/native/setup.ts"],"mappings":";;;;cAqBa,SAAS;iBA2BN,eAAe;iBAkCf;iBAeM,WAAW,QAAQ"}
1
+ {"version":3,"file":"setup.d.mts","names":[],"sources":["../../src/native/setup.ts"],"mappings":";;;;cAsBa,SAAS;iBA2BN,eAAe;iBAkCf;iBA0CM,WAAW,QAAQ"}
@@ -1,4 +1,5 @@
1
1
  import { cache, config } from "../setup/index.mjs";
2
+ import { keys } from "../constants/storage.mjs";
2
3
  import { Dimensions, PixelRatio, Platform } from "react-native";
3
4
  import { getAndroidId, getInstallReferrerAsync, getIosIdForVendorAsync } from "expo-application";
4
5
  import { DeviceType, deviceType, manufacturer, modelId, modelName, osName, osVersion } from "expo-device";
@@ -71,12 +72,38 @@ function getDeviceType() {
71
72
  default: return;
72
73
  }
73
74
  }
75
+ /**
76
+ * The install referrer's utm belongs to one launch: the first one that has the referrer in hand
77
+ * — normally the install launch, since `first_open` is the first event the app tracks and
78
+ * tracking it builds the first tags. That launch claims the referrer by writing a marker of its
79
+ * own, and holds the claim for the rest of the process, so every event of the launch and the
80
+ * visitor created from it carry the utm; every later launch finds the marker and sends the raw
81
+ * `install_referrer` alone. Cleared storage and a reinstall lose the marker and claim again,
82
+ * exactly as they make `first_open` fire again.
83
+ *
84
+ * Claimed only once a referrer has actually been resolved. The Install Referrer API needs the
85
+ * Play Store service, which is at its least available right after an install; a launch that
86
+ * could not reach it must not spend the claim on nothing, because the referrer stays readable
87
+ * for 90 days and a later launch can still take it — late attribution, rather than none. iOS
88
+ * has no referrer and so never claims.
89
+ *
90
+ * A marker of its own rather than `first_open_time`, so that nothing here depends on when the
91
+ * hook writes that one relative to the first `getTags`. The storage read waits for that call:
92
+ * `setupAnalytics` and this module's evaluation can run where no storage exists.
93
+ */
94
+ let installLaunch;
95
+ function claimInstallReferrer() {
96
+ if (config.storage.getItem(keys.install_referrer_claim_time)) return false;
97
+ config.storage.setItem(keys.install_referrer_claim_time, (/* @__PURE__ */ new Date()).toISOString());
98
+ return true;
99
+ }
74
100
  async function getTags() {
75
101
  const screen = Dimensions.get("screen");
76
102
  const screen_width = Math.floor(screen.width);
77
103
  const screen_height = Math.floor(screen.height);
78
104
  const install_referrer = await getInstallReferrer();
79
- const params = new URLSearchParams(install_referrer);
105
+ if (install_referrer && installLaunch === void 0) installLaunch = claimInstallReferrer();
106
+ const params = new URLSearchParams(installLaunch ? install_referrer : void 0);
80
107
  const tags = {
81
108
  os: `${osName} ${osVersion}`,
82
109
  os_name: osName ?? void 0,
@@ -1 +1 @@
1
- {"version":3,"file":"setup.mjs","names":[],"sources":["../../src/native/setup.ts"],"sourcesContent":["import 'expo-sqlite/localStorage/install';\nimport { getAndroidId, getInstallReferrerAsync, getIosIdForVendorAsync } from 'expo-application';\nimport { randomUUID } from 'expo-crypto';\nimport {\n DeviceType,\n deviceType,\n manufacturer,\n modelId,\n modelName,\n osName,\n osVersion,\n} from 'expo-device';\nimport { getCalendars, getLocales } from 'expo-localization';\nimport { getAdvertisingId } from 'expo-tracking-transparency';\nimport { Dimensions, PixelRatio, Platform } from 'react-native';\nimport { URLSearchParams } from 'react-native-url-polyfill';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n\n/**\n * `getTags` runs once per event now, and both of these reach for a native module. Neither answer\n * can change while the app is running, so each is resolved once and the promise is reused. A\n * failed lookup is not cached, so the next event tries again.\n */\nlet deviceIdPromise: Promise<string> | undefined;\nlet installReferrerPromise: Promise<string | undefined> | undefined;\n\nexport function getDeviceId(): Promise<string> {\n deviceIdPromise ??= resolveDeviceId().catch((error: unknown) => {\n deviceIdPromise = undefined;\n throw error;\n });\n return deviceIdPromise;\n}\n\nfunction getInstallReferrer(): Promise<string | undefined> {\n if (Platform.OS !== 'android') return Promise.resolve(undefined);\n installReferrerPromise ??= getInstallReferrerAsync().catch((error: unknown) => {\n installReferrerPromise = undefined;\n throw error;\n });\n return installReferrerPromise;\n}\n\nasync function resolveDeviceId(): Promise<string> {\n let deviceId: string | null = null;\n if (Platform.OS === 'ios') {\n deviceId = await getIosIdForVendorAsync();\n } else if (Platform.OS === 'android') {\n deviceId = getAndroidId();\n }\n if (!deviceId) {\n deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = randomUUID();\n localStorage.setItem('device_id', deviceId);\n }\n }\n return deviceId;\n}\n\nexport function getDeviceType(): string | undefined {\n switch (deviceType) {\n case DeviceType.PHONE:\n return 'mobile';\n case DeviceType.TABLET:\n return 'tablet';\n case DeviceType.DESKTOP:\n return 'desktop';\n case DeviceType.TV:\n return 'smarttv';\n default:\n return undefined;\n }\n}\n\nexport async function getTags(): Promise<TrackTags> {\n const screen = Dimensions.get('screen');\n const screen_width = Math.floor(screen.width);\n const screen_height = Math.floor(screen.height);\n\n const install_referrer = await getInstallReferrer();\n const params = new URLSearchParams(install_referrer);\n\n const tags: TrackTags = {\n os: `${osName} ${osVersion}`,\n os_name: osName ?? undefined,\n os_version: osVersion ?? undefined,\n device: modelName ?? undefined,\n device_id: await getDeviceId(),\n device_type: getDeviceType(),\n device_vendor: manufacturer ?? undefined,\n device_model_id: modelId ?? undefined,\n device_pixel_ratio: PixelRatio.get(),\n screen_width,\n screen_height,\n screen_resolution: `${screen_width}x${screen_height}`,\n release: config.release,\n language: getLocales().at(0)?.languageTag ?? 'en',\n time_zone: getCalendars().at(0)?.timeZone ?? 'UTC',\n // ads\n advertising_id: getAdvertisingId() ?? undefined,\n install_referrer,\n // utm params\n utm_source: params.get('utm_source') ?? undefined,\n utm_medium: params.get('utm_medium') ?? undefined,\n utm_campaign: params.get('utm_campaign') ?? undefined,\n utm_term: params.get('utm_term') ?? undefined,\n utm_content: params.get('utm_content') ?? undefined,\n utm_id: params.get('utm_id') ?? undefined,\n utm_source_platform: params.get('utm_source_platform') ?? undefined,\n utm_creative_format: params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic: params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,sBAAM,IAAI,IAAoB;AAEpC,MAAa,UAAmB;CAC9B,UAAU,QAAQ;EAChB,IAAI;GACF,OAAO,aAAa,QAAQ,GAAG;EACjC,QAAQ;GACN,QAAQ,MAAM,+BAA+B;GAC7C,OAAO,IAAI,IAAI,GAAG,KAAK;EACzB;CACF;CACA,UAAU,KAAK,UAAU;EACvB,IAAI;GACF,aAAa,QAAQ,KAAK,KAAK;EACjC,QAAQ;GACN,QAAQ,MAAM,+BAA+B;GAC7C,IAAI,IAAI,KAAK,KAAK;EACpB;CACF;AACF;;;;;;AAOA,IAAI;AACJ,IAAI;AAEJ,SAAgB,cAA+B;CAC7C,oBAAoB,gBAAgB,CAAC,CAAC,OAAO,UAAmB;EAC9D,kBAAkB,KAAA;EAClB,MAAM;CACR,CAAC;CACD,OAAO;AACT;AAEA,SAAS,qBAAkD;CACzD,IAAI,SAAS,OAAO,WAAW,OAAO,QAAQ,QAAQ,KAAA,CAAS;CAC/D,2BAA2B,wBAAwB,CAAC,CAAC,OAAO,UAAmB;EAC7E,yBAAyB,KAAA;EACzB,MAAM;CACR,CAAC;CACD,OAAO;AACT;AAEA,eAAe,kBAAmC;CAChD,IAAI,WAA0B;CAC9B,IAAI,SAAS,OAAO,OAClB,WAAW,MAAM,uBAAuB;MACnC,IAAI,SAAS,OAAO,WACzB,WAAW,aAAa;CAE1B,IAAI,CAAC,UAAU;EACb,WAAW,aAAa,QAAQ,WAAW;EAC3C,IAAI,CAAC,UAAU;GACb,WAAW,WAAW;GACtB,aAAa,QAAQ,aAAa,QAAQ;EAC5C;CACF;CACA,OAAO;AACT;AAEA,SAAgB,gBAAoC;CAClD,QAAQ,YAAR;EACE,KAAK,WAAW,OACd,OAAO;EACT,KAAK,WAAW,QACd,OAAO;EACT,KAAK,WAAW,SACd,OAAO;EACT,KAAK,WAAW,IACd,OAAO;EACT,SACE;CACJ;AACF;AAEA,eAAsB,UAA8B;CAClD,MAAM,SAAS,WAAW,IAAI,QAAQ;CACtC,MAAM,eAAe,KAAK,MAAM,OAAO,KAAK;CAC5C,MAAM,gBAAgB,KAAK,MAAM,OAAO,MAAM;CAE9C,MAAM,mBAAmB,MAAM,mBAAmB;CAClD,MAAM,SAAS,IAAI,gBAAgB,gBAAgB;CAEnD,MAAM,OAAkB;EACtB,IAAI,GAAG,OAAO,GAAG;EACjB,SAAS,UAAU,KAAA;EACnB,YAAY,aAAa,KAAA;EACzB,QAAQ,aAAa,KAAA;EACrB,WAAW,MAAM,YAAY;EAC7B,aAAa,cAAc;EAC3B,eAAe,gBAAgB,KAAA;EAC/B,iBAAiB,WAAW,KAAA;EAC5B,oBAAoB,WAAW,IAAI;EACnC;EACA;EACA,mBAAmB,GAAG,aAAa,GAAG;EACtC,SAAS,OAAO;EAChB,UAAU,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe;EAC7C,WAAW,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY;EAE7C,gBAAgB,iBAAiB,KAAK,KAAA;EACtC;EAEA,YAAY,OAAO,IAAI,YAAY,KAAK,KAAA;EACxC,YAAY,OAAO,IAAI,YAAY,KAAK,KAAA;EACxC,cAAc,OAAO,IAAI,cAAc,KAAK,KAAA;EAC5C,UAAU,OAAO,IAAI,UAAU,KAAK,KAAA;EACpC,aAAa,OAAO,IAAI,aAAa,KAAK,KAAA;EAC1C,QAAQ,OAAO,IAAI,QAAQ,KAAK,KAAA;EAChC,qBAAqB,OAAO,IAAI,qBAAqB,KAAK,KAAA;EAC1D,qBAAqB,OAAO,IAAI,qBAAqB,KAAK,KAAA;EAC1D,sBAAsB,OAAO,IAAI,sBAAsB,KAAK,KAAA;CAC9D;CAEA,MAAM,OAAO;CACb,OAAO;AACT"}
1
+ {"version":3,"file":"setup.mjs","names":[],"sources":["../../src/native/setup.ts"],"sourcesContent":["import 'expo-sqlite/localStorage/install';\nimport { getAndroidId, getInstallReferrerAsync, getIosIdForVendorAsync } from 'expo-application';\nimport { randomUUID } from 'expo-crypto';\nimport {\n DeviceType,\n deviceType,\n manufacturer,\n modelId,\n modelName,\n osName,\n osVersion,\n} from 'expo-device';\nimport { getCalendars, getLocales } from 'expo-localization';\nimport { getAdvertisingId } from 'expo-tracking-transparency';\nimport { Dimensions, PixelRatio, Platform } from 'react-native';\nimport { URLSearchParams } from 'react-native-url-polyfill';\nimport { keys } from '../constants/storage';\nimport { type Storage, cache, config } from '../setup/index';\nimport type { TrackTags } from '../track/types';\n\nconst map = new Map<string, string>();\n\nexport const storage: Storage = {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n console.error('localStorage is not available');\n return map.get(key) ?? null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n console.error('localStorage is not available');\n map.set(key, value);\n }\n },\n};\n\n/**\n * `getTags` runs once per event now, and both of these reach for a native module. Neither answer\n * can change while the app is running, so each is resolved once and the promise is reused. A\n * failed lookup is not cached, so the next event tries again.\n */\nlet deviceIdPromise: Promise<string> | undefined;\nlet installReferrerPromise: Promise<string | undefined> | undefined;\n\nexport function getDeviceId(): Promise<string> {\n deviceIdPromise ??= resolveDeviceId().catch((error: unknown) => {\n deviceIdPromise = undefined;\n throw error;\n });\n return deviceIdPromise;\n}\n\nfunction getInstallReferrer(): Promise<string | undefined> {\n if (Platform.OS !== 'android') return Promise.resolve(undefined);\n installReferrerPromise ??= getInstallReferrerAsync().catch((error: unknown) => {\n installReferrerPromise = undefined;\n throw error;\n });\n return installReferrerPromise;\n}\n\nasync function resolveDeviceId(): Promise<string> {\n let deviceId: string | null = null;\n if (Platform.OS === 'ios') {\n deviceId = await getIosIdForVendorAsync();\n } else if (Platform.OS === 'android') {\n deviceId = getAndroidId();\n }\n if (!deviceId) {\n deviceId = localStorage.getItem('device_id');\n if (!deviceId) {\n deviceId = randomUUID();\n localStorage.setItem('device_id', deviceId);\n }\n }\n return deviceId;\n}\n\nexport function getDeviceType(): string | undefined {\n switch (deviceType) {\n case DeviceType.PHONE:\n return 'mobile';\n case DeviceType.TABLET:\n return 'tablet';\n case DeviceType.DESKTOP:\n return 'desktop';\n case DeviceType.TV:\n return 'smarttv';\n default:\n return undefined;\n }\n}\n\n/**\n * The install referrer's utm belongs to one launch: the first one that has the referrer in hand\n * — normally the install launch, since `first_open` is the first event the app tracks and\n * tracking it builds the first tags. That launch claims the referrer by writing a marker of its\n * own, and holds the claim for the rest of the process, so every event of the launch and the\n * visitor created from it carry the utm; every later launch finds the marker and sends the raw\n * `install_referrer` alone. Cleared storage and a reinstall lose the marker and claim again,\n * exactly as they make `first_open` fire again.\n *\n * Claimed only once a referrer has actually been resolved. The Install Referrer API needs the\n * Play Store service, which is at its least available right after an install; a launch that\n * could not reach it must not spend the claim on nothing, because the referrer stays readable\n * for 90 days and a later launch can still take it — late attribution, rather than none. iOS\n * has no referrer and so never claims.\n *\n * A marker of its own rather than `first_open_time`, so that nothing here depends on when the\n * hook writes that one relative to the first `getTags`. The storage read waits for that call:\n * `setupAnalytics` and this module's evaluation can run where no storage exists.\n */\nlet installLaunch: boolean | undefined;\n\nfunction claimInstallReferrer(): boolean {\n if (config.storage.getItem(keys.install_referrer_claim_time)) return false;\n config.storage.setItem(keys.install_referrer_claim_time, new Date().toISOString());\n return true;\n}\n\nexport async function getTags(): Promise<TrackTags> {\n const screen = Dimensions.get('screen');\n const screen_width = Math.floor(screen.width);\n const screen_height = Math.floor(screen.height);\n\n // The install referrer never changes, and it used to be spread into `utm_*` on every launch —\n // so every session of an Android install reported the install campaign as its own acquisition\n // for as long as the app stayed installed, and a report reading a session's tags could not tell\n // the install from the thousandth open. The campaign is the install's touch, not every\n // session's.\n const install_referrer = await getInstallReferrer();\n if (install_referrer && installLaunch === undefined) installLaunch = claimInstallReferrer();\n const params = new URLSearchParams(installLaunch ? install_referrer : undefined);\n\n const tags: TrackTags = {\n os: `${osName} ${osVersion}`,\n os_name: osName ?? undefined,\n os_version: osVersion ?? undefined,\n device: modelName ?? undefined,\n device_id: await getDeviceId(),\n device_type: getDeviceType(),\n device_vendor: manufacturer ?? undefined,\n device_model_id: modelId ?? undefined,\n device_pixel_ratio: PixelRatio.get(),\n screen_width,\n screen_height,\n screen_resolution: `${screen_width}x${screen_height}`,\n release: config.release,\n language: getLocales().at(0)?.languageTag ?? 'en',\n time_zone: getCalendars().at(0)?.timeZone ?? 'UTC',\n // ads\n advertising_id: getAdvertisingId() ?? undefined,\n install_referrer,\n // utm params: from the install referrer, on the launch that claimed it\n utm_source: params.get('utm_source') ?? undefined,\n utm_medium: params.get('utm_medium') ?? undefined,\n utm_campaign: params.get('utm_campaign') ?? undefined,\n utm_term: params.get('utm_term') ?? undefined,\n utm_content: params.get('utm_content') ?? undefined,\n utm_id: params.get('utm_id') ?? undefined,\n utm_source_platform: params.get('utm_source_platform') ?? undefined,\n utm_creative_format: params.get('utm_creative_format') ?? undefined,\n utm_marketing_tactic: params.get('utm_marketing_tactic') ?? undefined,\n };\n\n cache.tags = tags;\n return tags;\n}\n"],"mappings":"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"}
@@ -6,11 +6,11 @@ const require_hooks_use_outbound_click_analytics = require("../hooks/use-outboun
6
6
  const require_hooks_use_uet_id_sync = require("../hooks/use-uet-id-sync.cjs");
7
7
  const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
8
8
  let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
9
10
  let next_navigation_js = require("next/navigation.js");
10
11
  let next_script_js = require("next/script.js");
11
12
  next_script_js = require_runtime.__toESM(next_script_js, 1);
12
13
  let next_web_vitals_js = require("next/web-vitals.js");
13
- let react_jsx_runtime = require("react/jsx-runtime");
14
14
  //#region src/next/index.tsx
15
15
  /**
16
16
  * Its own component under a Suspense boundary: `useSearchParams` in a statically rendered route
@@ -4,10 +4,10 @@ import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics
4
4
  import { useUETIdSync } from "../hooks/use-uet-id-sync.mjs";
5
5
  import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
6
6
  import { Suspense } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
8
  import { usePathname, useSearchParams } from "next/navigation.js";
8
9
  import Script from "next/script.js";
9
10
  import { useReportWebVitals } from "next/web-vitals.js";
10
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
11
  //#region src/next/index.tsx
12
12
  /**
13
13
  * Its own component under a Suspense boundary: `useSearchParams` in a statically rendered route
@@ -4,8 +4,8 @@ const require_hooks_use_outbound_click_analytics = require("../hooks/use-outboun
4
4
  const require_hooks_use_report_web_vitals = require("../hooks/use-report-web-vitals.cjs");
5
5
  const require_hooks_use_uet_id_sync = require("../hooks/use-uet-id-sync.cjs");
6
6
  const require_hooks_use_web_analytics = require("../hooks/use-web-analytics.cjs");
7
- let react_jsx_runtime = require("react/jsx-runtime");
8
7
  let react_router = require("react-router");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
9
  //#region src/react-router/index.tsx
10
10
  function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, uetTagId, uetCustomerId, hotjarId, facebookAppId, reportWebVitals = true }) {
11
11
  const { pathname, search } = (0, react_router.useLocation)();
@@ -3,8 +3,8 @@ import { useOutboundClickAnalytics } from "../hooks/use-outbound-click-analytics
3
3
  import { useReportWebVitals } from "../hooks/use-report-web-vitals.mjs";
4
4
  import { useUETIdSync } from "../hooks/use-uet-id-sync.mjs";
5
5
  import { useWebAnalytics } from "../hooks/use-web-analytics.mjs";
6
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
6
  import { useLocation } from "react-router";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/react-router/index.tsx
9
9
  function Analytics({ gaId, gaSrc, nonce, debugMode, metaPixelId, redditPixelId, linkedInPartnerId, uetTagId, uetCustomerId, hotjarId, facebookAppId, reportWebVitals = true }) {
10
10
  const { pathname, search } = useLocation();
@@ -38,18 +38,38 @@ async function captureTags() {
38
38
  return require_setup_index.cache.tags ?? {};
39
39
  }
40
40
  }
41
+ /**
42
+ * The `session_start` a failed batch took down with it, kept for the next batch of the same
43
+ * session. `fetch` has already retried the transient failures by the time a batch fails here;
44
+ * what remains is a batch the server rejected outright — one invalid event fails the whole batch,
45
+ * and `session_start` with it — or a network that stayed down past the retries. Neither is a
46
+ * reason to lose the session's one attribution record, so it goes out again, with the tags and
47
+ * timestamp of the moment the session actually began, under the same session id. Held in memory
48
+ * only: a reload loses it, and with it the session_start — the same as before this existed.
49
+ */
50
+ let pendingStart;
41
51
  async function sendEvents(events) {
52
+ /** The announcement this batch carries, if any, so the catch below can hold it back. */
53
+ let announcement;
42
54
  try {
43
55
  if (events.length === 0) return;
44
- const firstTimestamp = events[0].timestamp;
45
- const { id: session_id, started } = require_setup_session.getSession().touch(Date.parse(firstTimestamp), Date.parse(events[events.length - 1].timestamp));
46
- if (started) events.unshift({
47
- name: "session_start",
48
- properties: {},
49
- options: { enableThirdPartyTracking: false },
50
- tags: captureTags(),
51
- timestamp: firstTimestamp
52
- });
56
+ const opening = events[0];
57
+ const { id: session_id, started } = require_setup_session.getSession().touch(Date.parse(opening.timestamp), Date.parse(events[events.length - 1].timestamp));
58
+ if (started || pendingStart?.session_id === session_id) {
59
+ const start = pendingStart?.session_id === session_id ? pendingStart.item : {
60
+ name: "session_start",
61
+ properties: {},
62
+ options: { enableThirdPartyTracking: false },
63
+ tags: opening.tags,
64
+ timestamp: opening.timestamp
65
+ };
66
+ pendingStart = void 0;
67
+ announcement = {
68
+ session_id,
69
+ item: start
70
+ };
71
+ events.unshift(start);
72
+ }
53
73
  await getTokenBucket().removeTokens();
54
74
  const visitor_id = (await require_visitor_index.getVisitor()).id;
55
75
  const dto = await Promise.all(events.map(async (event) => ({
@@ -93,6 +113,7 @@ async function sendEvents(events) {
93
113
  }
94
114
  } catch (e) {
95
115
  if (e instanceof Error) console.log(e.message);
116
+ if (announcement) pendingStart = announcement;
96
117
  events.forEach((event) => event.options.onError?.(e));
97
118
  }
98
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","fetch","IGNORED_EVENTS","keys"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["TokenBucket","config","cache","getSession","getVisitor","fetch","IGNORED_EVENTS","keys"],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The `session_start` a failed batch took down with it, kept for the next batch of the same\n * session. `fetch` has already retried the transient failures by the time a batch fails here;\n * what remains is a batch the server rejected outright — one invalid event fails the whole batch,\n * and `session_start` with it — or a network that stayed down past the retries. Neither is a\n * reason to lose the session's one attribution record, so it goes out again, with the tags and\n * timestamp of the moment the session actually began, under the same session id. Held in memory\n * only: a reload loses it, and with it the session_start — the same as before this existed.\n */\nlet pendingStart: { session_id: string; item: Item } | undefined;\n\nasync function sendEvents(events: Item[]) {\n /** The announcement this batch carries, if any, so the catch below can hold it back. */\n let announcement: { session_id: string; item: Item } | undefined;\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const opening = events[0];\n const { id: session_id, started } = getSession().touch(\n Date.parse(opening.timestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started || pendingStart?.session_id === session_id) {\n // `session_start` is derived from the event that opened the session, as GA4 derives it\n // from the hit that did: when that event happened is when the session began, and the page\n // it was captured on is where. Neither is read now — this batch goes out up to `delay` ms\n // later, a batch held in a frozen tab far later still, and a landing page that redirects\n // inside that window would stamp the session's one attribution record with the URL the utm\n // parameters were already stripped from.\n const start: Item =\n pendingStart?.session_id === session_id\n ? pendingStart.item\n : {\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: opening.tags,\n timestamp: opening.timestamp,\n };\n pendingStart = undefined;\n announcement = { session_id, item: start };\n events.unshift(start);\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // The other events are reported lost and stay lost; `session_start` alone is worth carrying\n // over, because it is the session's only attribution record — a session without one has no\n // channel, and its every later event drops out of an attribution join.\n if (announcement) pendingStart = announcement;\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBA8JG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAyLG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBA8JG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/track/index.ts"],"mappings":";;UASiB;EACf;EACA,aAAa,WAAW;EACxB,WAAW;;iBAyLG,MAAM,UAAU,YAAY,WAC1C,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS;iBAiBW,WAAW,UAAU,YAAY,WACrD,MAAM,UAAU,IAChB,aAAa,gBAAgB,IAC7B,UAAS,eAA6B;iBAOxB,WAAW,UAAU,YAAY,WAC/C,MAAM,UAAU,IAChB,aAAa,gBAAgB"}
@@ -37,18 +37,38 @@ async function captureTags() {
37
37
  return cache.tags ?? {};
38
38
  }
39
39
  }
40
+ /**
41
+ * The `session_start` a failed batch took down with it, kept for the next batch of the same
42
+ * session. `fetch` has already retried the transient failures by the time a batch fails here;
43
+ * what remains is a batch the server rejected outright — one invalid event fails the whole batch,
44
+ * and `session_start` with it — or a network that stayed down past the retries. Neither is a
45
+ * reason to lose the session's one attribution record, so it goes out again, with the tags and
46
+ * timestamp of the moment the session actually began, under the same session id. Held in memory
47
+ * only: a reload loses it, and with it the session_start — the same as before this existed.
48
+ */
49
+ let pendingStart;
40
50
  async function sendEvents(events) {
51
+ /** The announcement this batch carries, if any, so the catch below can hold it back. */
52
+ let announcement;
41
53
  try {
42
54
  if (events.length === 0) return;
43
- const firstTimestamp = events[0].timestamp;
44
- const { id: session_id, started } = getSession().touch(Date.parse(firstTimestamp), Date.parse(events[events.length - 1].timestamp));
45
- if (started) events.unshift({
46
- name: "session_start",
47
- properties: {},
48
- options: { enableThirdPartyTracking: false },
49
- tags: captureTags(),
50
- timestamp: firstTimestamp
51
- });
55
+ const opening = events[0];
56
+ const { id: session_id, started } = getSession().touch(Date.parse(opening.timestamp), Date.parse(events[events.length - 1].timestamp));
57
+ if (started || pendingStart?.session_id === session_id) {
58
+ const start = pendingStart?.session_id === session_id ? pendingStart.item : {
59
+ name: "session_start",
60
+ properties: {},
61
+ options: { enableThirdPartyTracking: false },
62
+ tags: opening.tags,
63
+ timestamp: opening.timestamp
64
+ };
65
+ pendingStart = void 0;
66
+ announcement = {
67
+ session_id,
68
+ item: start
69
+ };
70
+ events.unshift(start);
71
+ }
52
72
  await getTokenBucket().removeTokens();
53
73
  const visitor_id = (await getVisitor()).id;
54
74
  const dto = await Promise.all(events.map(async (event) => ({
@@ -92,6 +112,7 @@ async function sendEvents(events) {
92
112
  }
93
113
  } catch (e) {
94
114
  if (e instanceof Error) console.log(e.message);
115
+ if (announcement) pendingStart = announcement;
95
116
  events.forEach((event) => event.options.onError?.(e));
96
117
  }
97
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\nasync function sendEvents(events: Item[]) {\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const firstTimestamp = events[0].timestamp;\n const { id: session_id, started } = getSession().touch(\n Date.parse(firstTimestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started) {\n events.unshift({\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: captureTags(),\n // The session began with the event that opened it, not at this moment: a batch held in a\n // frozen tab would otherwise announce its session later than the events inside it.\n timestamp: firstTimestamp,\n });\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/track/index.ts"],"sourcesContent":["import { TokenBucket, fetch } from '@shware/utils';\nimport { keys } from '../constants/storage';\nimport type { CreateTrackEventDTO } from '../schema/index';\nimport { cache, config } from '../setup/index';\nimport { getSession } from '../setup/session';\nimport { IGNORED_EVENTS } from '../third-parties/ignored-events';\nimport { getVisitor } from '../visitor/index';\nimport type { EventName, TrackEventResponse, TrackName, TrackProperties, TrackTags } from './types';\n\nexport interface TrackOptions {\n enableThirdPartyTracking?: boolean;\n onSucceed?: (response?: TrackEventResponse[number]) => void;\n onError?: (error: unknown) => void;\n}\n\nconst defaultOptions: TrackOptions = {};\n\nlet tokenBucket: TokenBucket | undefined;\n\n/**\n * The rate limiter, built on the first send rather than at module scope: its\n * constructor starts a refill `setInterval`, and Cloudflare Workers refuse to\n * set a timer outside a request handler for the same reason they refuse to\n * generate random values there — see `setup/session.ts`.\n */\nfunction getTokenBucket() {\n return (tokenBucket ??= new TokenBucket({ rate: 1, capacity: 20, requested: 2 }));\n}\n\ntype Item = {\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n name: TrackName<any>;\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any\n properties: TrackProperties<any>;\n tags: Promise<TrackTags>;\n timestamp: string;\n options: TrackOptions;\n};\n\n/**\n * Tags belong to the moment the event happened, not to the moment its batch goes out: a queued\n * event waits up to `delay` ms, and a single page app can navigate in that window, which would\n * stamp every pending event with the URL of the page the user has already left.\n *\n * The promise then sits in the queue with nothing awaiting it, so a failure has to be absorbed\n * here — an unhandled rejection would surface as a global error long before `sendEvents` could\n * catch it. Falling back to the last built tags keeps the event, minus whatever changed since.\n */\nasync function captureTags(): Promise<TrackTags> {\n try {\n return await config.getTags();\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n return cache.tags ?? {};\n }\n}\n\n/**\n * The `session_start` a failed batch took down with it, kept for the next batch of the same\n * session. `fetch` has already retried the transient failures by the time a batch fails here;\n * what remains is a batch the server rejected outright — one invalid event fails the whole batch,\n * and `session_start` with it — or a network that stayed down past the retries. Neither is a\n * reason to lose the session's one attribution record, so it goes out again, with the tags and\n * timestamp of the moment the session actually began, under the same session id. Held in memory\n * only: a reload loses it, and with it the session_start — the same as before this existed.\n */\nlet pendingStart: { session_id: string; item: Item } | undefined;\n\nasync function sendEvents(events: Item[]) {\n /** The announcement this batch carries, if any, so the catch below can hold it back. */\n let announcement: { session_id: string; item: Item } | undefined;\n try {\n if (events.length === 0) return;\n\n // One read-modify-write of the stored session for the whole batch: it answers which session\n // these events belong to and whether this batch is the one that started it. Timed by the\n // events themselves rather than by this moment — a tab frozen in the background can hold a\n // batch for far longer than `delay`, and those events belong to the session they happened in.\n const opening = events[0];\n const { id: session_id, started } = getSession().touch(\n Date.parse(opening.timestamp),\n Date.parse(events[events.length - 1].timestamp)\n );\n if (started || pendingStart?.session_id === session_id) {\n // `session_start` is derived from the event that opened the session, as GA4 derives it\n // from the hit that did: when that event happened is when the session began, and the page\n // it was captured on is where. Neither is read now — this batch goes out up to `delay` ms\n // later, a batch held in a frozen tab far later still, and a landing page that redirects\n // inside that window would stamp the session's one attribution record with the URL the utm\n // parameters were already stripped from.\n const start: Item =\n pendingStart?.session_id === session_id\n ? pendingStart.item\n : {\n name: 'session_start',\n properties: {},\n options: { enableThirdPartyTracking: false },\n tags: opening.tags,\n timestamp: opening.timestamp,\n };\n pendingStart = undefined;\n announcement = { session_id, item: start };\n events.unshift(start);\n }\n\n await getTokenBucket().removeTokens();\n\n const visitor_id = (await getVisitor()).id;\n\n const dto: CreateTrackEventDTO = await Promise.all(\n events.map(async (event) => ({\n name: event.name,\n properties: event.properties,\n tags: await event.tags,\n visitor_id,\n session_id,\n platform: config.platform,\n environment: config.environment,\n timestamp: event.timestamp,\n }))\n );\n\n const response = await fetch(`${config.endpoint}/events`, {\n method: 'POST',\n credentials: 'include',\n // Survive the page being unloaded mid-flight: a batch waits up to `delay` ms, so closing\n // the tab inside that window would otherwise abort the request and lose every event in it.\n // The body stays far under keepalive's 64KB in-flight budget at 100 events per batch.\n keepalive: true,\n headers: await config.getHeaders(),\n body: JSON.stringify(dto),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send track event: ${response.status} ${await response.text()}`);\n }\n\n const data = (await response.json()) as TrackEventResponse;\n\n let index = 0;\n while (events.length > 0) {\n const event = events.shift();\n if (!event) {\n index++;\n continue;\n }\n const { options, name, properties } = event;\n const eventId = data.at(index)?.id;\n options.onSucceed?.(eventId ? { id: eventId } : undefined);\n index++;\n // An explicit false, not falsiness: a caller passing `{ onSucceed }` replaces the options\n // object wholesale, and leaving the flag out must not silently switch forwarding off.\n if (options.enableThirdPartyTracking === false || IGNORED_EVENTS.includes(name)) {\n continue;\n }\n config.thirdPartyTrackers.forEach((tracker) => {\n try {\n tracker(name, properties, eventId);\n } catch (e: unknown) {\n // A third-party script does not get to take the rest of the batch with it. This loop is\n // still draining `events` with `shift`, so a throw would escape to the catch below and\n // report failure to whatever is left in the queue — for events the server has already\n // accepted, and after the ones ahead of them were told they succeeded.\n if (e instanceof Error) console.log(e.message);\n }\n });\n }\n } catch (e: unknown) {\n if (e instanceof Error) console.log(e.message);\n // The other events are reported lost and stay lost; `session_start` alone is worth carrying\n // over, because it is the session's only attribution record — a session without one has no\n // channel, and its every later event drops out of an attribution join.\n if (announcement) pendingStart = announcement;\n events.forEach((event) => event.options.onError?.(e));\n }\n}\n\nconst batch = 10;\nconst delay = 2000;\nconst list: Item[] = [];\nlet timer: ReturnType<typeof setTimeout> | null = null;\n\n/**\n * Both paths into a send go through here, so a batch that fills up cancels the timer the\n * previous push armed rather than leaving it to wake up on its own with nothing to send.\n */\nfunction flush() {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (list.length === 0) return;\n const copy = [...list];\n list.length = 0;\n void sendEvents(copy);\n}\n\nexport function track<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n list.push({\n name,\n properties,\n options,\n tags: captureTags(),\n timestamp: new Date().toISOString(),\n });\n if (list.length >= batch) {\n flush();\n return;\n }\n if (timer) clearTimeout(timer);\n timer = setTimeout(flush, delay);\n}\n\nexport async function trackAsync<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>,\n options: TrackOptions = defaultOptions\n) {\n await sendEvents([\n { name, properties, options, tags: captureTags(), timestamp: new Date().toISOString() },\n ]);\n}\n\nexport function sendBeacon<T extends EventName = EventName>(\n name: TrackName<T>,\n properties?: TrackProperties<T>\n) {\n // The visitor id is persisted, so a returning visitor already has one before `getVisitor` has\n // finished its round trip for this page. Requiring the in-memory copy threw away exactly the\n // events this function exists for: everything a visit accrues before its first batch comes\n // back, which for a short visit is the whole of it.\n const stored = config.storage.getItem(keys.visitor_id);\n const visitor_id = cache.visitor?.id ?? (stored && stored !== 'undefined' ? stored : undefined);\n if (!visitor_id) return;\n\n const dto: CreateTrackEventDTO = [\n {\n name,\n properties,\n // Tags are worth less than the event carrying them: an empty set still reports the\n // engagement, and every field in `tagsSchema` is optional.\n tags: cache.tags ?? {},\n visitor_id,\n session_id: getSession().extend(),\n platform: config.platform,\n environment: config.environment,\n timestamp: new Date().toISOString(),\n },\n ];\n const blob = new Blob([JSON.stringify(dto)], { type: 'application/json' });\n const success = navigator.sendBeacon(`${config.endpoint}/events`, blob);\n if (success) return;\n console.warn('Failed to send beacon', name, properties);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/analytics",
3
- "version": "8.7.0",
3
+ "version": "8.7.2",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",