@tokenoftrust/storefront-runner 2.2.17 → 2.2.19

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.17",
3
+ "version": "2.2.19",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -6,6 +6,15 @@
6
6
  * payload to one exact collection endpoint. The fixed device value groups events
7
7
  * as non-identifying storefront traffic; it is never derived from a browser,
8
8
  * person, account, persisted client value, network address, or device signal.
9
+ *
10
+ * Channel ATTRIBUTION (UTM tokens + the referrer's origin) is allowed through as
11
+ * plain `event_properties`: it is a minimized, campaign-scoped label already
12
+ * validated upstream (`events.ts` `validatePageAttribution`), not a visitor or
13
+ * device signal, and this adapter re-screens each value for PII before sending
14
+ * it regardless. A persistent/session identity is a different thing entirely —
15
+ * it stays out, on purpose: `device_id` remains the fixed, non-identifying value
16
+ * below and there is still no `session_id`, `user_id`, or any persisted client
17
+ * value anywhere in this file.
9
18
  */
10
19
  import type { CommerceEventBroker, CommerceEventSubscription } from "./commerce-broker.js";
11
20
  import {
@@ -47,13 +56,23 @@ export const AMPLITUDE_HTTP_EVENT_TYPES = Object.freeze([
47
56
  "storefront.action.activated.v1",
48
57
  ] as const);
49
58
 
59
+ /** The flat channel-attribution keys this adapter will forward, mirroring `events.ts` UTM_FIELDS. */
60
+ export type AmplitudeAttributionProperties = Partial<{
61
+ utm_source: string;
62
+ utm_medium: string;
63
+ utm_campaign: string;
64
+ utm_term: string;
65
+ utm_content: string;
66
+ referrer_origin: string;
67
+ }>;
68
+
50
69
  export interface AmplitudeHttpEvent {
51
70
  event_type: "page_view" | (typeof APPROVED_MARKETING_ACTION_NAMES)[number];
52
71
  device_id: typeof ANONYMOUS_DEVICE_ID;
53
72
  insert_id: string;
54
73
  /** Occurrence time, epoch milliseconds. */
55
74
  time: number;
56
- event_properties: GovernedEventProperties & { page_path?: string };
75
+ event_properties: GovernedEventProperties & { page_path?: string } & AmplitudeAttributionProperties;
57
76
  }
58
77
 
59
78
  export const AMPLITUDE_DEDUPE_CAPACITY = 512 as const;
@@ -104,6 +123,44 @@ class BoundedInsertIds {
104
123
  }
105
124
  }
106
125
 
126
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
128
+ }
129
+
130
+ /**
131
+ * Flatten the already-validated `data.attribution` object (`events.ts`
132
+ * `validatePageAttribution` — a minimized referrer origin plus up to five UTM
133
+ * tokens) into Amplitude's flat `event_properties`. Re-screened for PII here
134
+ * rather than trusted from upstream validation alone, matching `isSafePagePath`'s
135
+ * own double-check: a field that resembles PII is dropped individually rather
136
+ * than failing the whole page-view event.
137
+ */
138
+ function extractAttributionProperties(data: Record<string, unknown>): AmplitudeAttributionProperties {
139
+ const attribution = data["attribution"];
140
+ if (!isPlainObject(attribution)) return {};
141
+ const out: AmplitudeAttributionProperties = {};
142
+
143
+ const utm = attribution["utm"];
144
+ if (isPlainObject(utm)) {
145
+ for (const key of ["source", "medium", "campaign", "term", "content"] as const) {
146
+ const value = utm[key];
147
+ if (typeof value === "string" && value.length > 0 && !resemblesManagedAnalyticsPii(value)) {
148
+ out[`utm_${key}`] = value;
149
+ }
150
+ }
151
+ }
152
+
153
+ const referrer = attribution["referrer"];
154
+ if (isPlainObject(referrer)) {
155
+ const origin = referrer["origin"];
156
+ if (typeof origin === "string" && origin.length > 0 && !resemblesManagedAnalyticsPii(origin)) {
157
+ out.referrer_origin = origin;
158
+ }
159
+ }
160
+
161
+ return out;
162
+ }
163
+
107
164
  function isSafePagePath(value: unknown): value is string {
108
165
  return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") &&
109
166
  !value.includes("\\") && !value.includes("?") && !value.includes("#") &&
@@ -166,7 +223,12 @@ export function mapCommerceEventToAmplitude(
166
223
  }
167
224
  const path = event.data["path"];
168
225
  if (!isSafePagePath(path)) return null;
169
- return { ...base, event_type: "page_view", event_properties: { ...governed, page_path: path } };
226
+ const attribution = extractAttributionProperties(event.data);
227
+ return {
228
+ ...base,
229
+ event_type: "page_view",
230
+ event_properties: { ...governed, page_path: path, ...attribution },
231
+ };
170
232
  }
171
233
 
172
234
  export interface RegisterAmplitudeHttpDestinationInput {