@funnelsgrove/analytics 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -6,7 +6,8 @@ Shared public analytics SDK helpers for funnels.
6
6
 
7
7
  - Build distributable output with `npm run build --workspace @funnelsgrove/analytics`.
8
8
  - Publish from the repo root with `npm publish --workspace @funnelsgrove/analytics --access public`.
9
- - Local repo installs still resolve this package through npm workspaces when another workspace depends on version `0.1.0`.
9
+ - Local repo installs still resolve this package through npm workspaces when another workspace depends on version `0.1.1`.
10
+ - The build normalizes generated relative ESM imports to explicit `.js` files for published package consumers.
10
11
 
11
12
  ## Use This Package For
12
13
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './services/runtime-api.config';
2
- export * from './services/sdk-user-id.storage';
3
- export * from './services/bootstrap-public-analytics-user';
4
- export * from './services/public-analytics-sdk.service';
1
+ export * from './services/runtime-api.config.js';
2
+ export * from './services/sdk-user-id.storage.js';
3
+ export * from './services/bootstrap-public-analytics-user.js';
4
+ export * from './services/public-analytics-sdk.service.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export * from './services/runtime-api.config';
2
- export * from './services/sdk-user-id.storage';
3
- export * from './services/bootstrap-public-analytics-user';
4
- export * from './services/public-analytics-sdk.service';
1
+ export * from './services/runtime-api.config.js';
2
+ export * from './services/sdk-user-id.storage.js';
3
+ export * from './services/bootstrap-public-analytics-user.js';
4
+ export * from './services/public-analytics-sdk.service.js';
@@ -1,4 +1,4 @@
1
- import type { PublicSdkIdentifyInput } from './public-analytics-sdk.service';
1
+ import type { PublicSdkIdentifyInput } from './public-analytics-sdk.service.js';
2
2
  export type BootstrapPublicAnalyticsUserClient = {
3
3
  identify(input?: PublicSdkIdentifyInput): Promise<string>;
4
4
  };
@@ -1,4 +1,4 @@
1
- import { publicAnalyticsSdk } from './public-analytics-sdk.service';
1
+ import { publicAnalyticsSdk } from './public-analytics-sdk.service.js';
2
2
  export const bootstrapPublicAnalyticsUser = async (input = {}, client = publicAnalyticsSdk) => {
3
3
  return client.identify(input);
4
4
  };
@@ -10,11 +10,13 @@ export type PublicSdkTrackEventInput = {
10
10
  payload?: Record<string, unknown>;
11
11
  context?: Record<string, unknown>;
12
12
  metadata?: Record<string, unknown>;
13
+ featureFlags?: Record<string, string | null | undefined>;
13
14
  };
14
15
  export type PublicSdkStepStartInput = {
15
16
  stepId: string;
16
17
  stepName: string;
17
18
  startedAt: string;
19
+ featureFlags?: Record<string, string | null | undefined>;
18
20
  };
19
21
  export type PublicSdkStepEndInput = {
20
22
  stepId: string;
@@ -22,9 +24,10 @@ export type PublicSdkStepEndInput = {
22
24
  startedAt: string;
23
25
  endedAt: string;
24
26
  selected?: Record<string, unknown>;
27
+ featureFlags?: Record<string, string | null | undefined>;
25
28
  };
26
29
  export type PublicSdkIdentifyInput = {
27
- externalUserId?: string;
30
+ user_id?: string;
28
31
  email?: string;
29
32
  fullName?: string;
30
33
  metadata?: Record<string, unknown>;
@@ -35,19 +38,27 @@ export type PublicAnalyticsSdkOptions = {
35
38
  };
36
39
  export declare class PublicAnalyticsSdkService {
37
40
  private readonly options;
38
- private readonly endpoint;
39
41
  private readonly eventQueue;
40
42
  private flushTimerId;
41
43
  private inFlightFlush;
42
- private externalUserId;
44
+ private userId;
43
45
  constructor(options?: Partial<PublicAnalyticsSdkOptions>);
44
- getCurrentExternalUserId(): string;
46
+ getCurrentUserId(): string;
45
47
  identify(input?: PublicSdkIdentifyInput): Promise<string>;
48
+ fetchCurrentUserState(input?: {
49
+ user_id?: string;
50
+ }): Promise<{
51
+ user: Record<string, unknown> | null;
52
+ events: unknown[];
53
+ subscriptions: unknown[];
54
+ payments: unknown[];
55
+ } | null>;
46
56
  track(event: PublicSdkTrackEventInput): string | null;
47
57
  trackStepStarted(input: PublicSdkStepStartInput): string | null;
48
58
  trackStepCompleted(input: PublicSdkStepEndInput): string | null;
49
59
  flush(): Promise<number>;
50
- private ensureExternalUserId;
60
+ private ensureUserId;
61
+ private resolveBootstrapUserId;
51
62
  private scheduleFlush;
52
63
  private clearFlushTimer;
53
64
  private flushQueue;
@@ -1,17 +1,18 @@
1
- import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, getFunnelSdkPublishableKey, } from './runtime-api.config';
2
- import { canUseDom, getOrCreateUserId, persistUserId, } from './sdk-user-id.storage';
1
+ import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableKey, hasPostHogRuntimeConfig, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from './runtime-api.config.js';
2
+ import { canUseDom, getOrCreateUserId, persistUserId, readStoredUserId, } from './sdk-user-id.storage.js';
3
3
  const DEFAULT_OPTIONS = {
4
4
  batchSize: 20,
5
5
  flushIntervalMs: 1500,
6
6
  };
7
+ const POSTHOG_BATCH_ENDPOINT = '/batch/';
7
8
  const asRecord = (value) => {
8
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
9
10
  return {};
10
11
  }
11
12
  return value;
12
13
  };
13
14
  const asString = (value) => {
14
- if (typeof value !== "string") {
15
+ if (typeof value !== 'string') {
15
16
  return null;
16
17
  }
17
18
  const trimmed = value.trim();
@@ -28,60 +29,118 @@ const toIsoOrNow = (value) => {
28
29
  };
29
30
  const buildEventId = () => {
30
31
  var _a;
31
- if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === "function") {
32
- return `sdk_evt_${window.crypto.randomUUID().replace(/-/g, "")}`;
32
+ if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
33
+ return `sdk_evt_${window.crypto.randomUUID().replace(/-/g, '')}`;
33
34
  }
34
35
  return `sdk_evt_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
35
36
  };
37
+ const toPostHogEventName = (eventType) => {
38
+ switch (eventType) {
39
+ case 'funnel_start':
40
+ return 'funnel_started';
41
+ case 'step_start':
42
+ return 'step_started';
43
+ case 'step_end':
44
+ return 'step_completed';
45
+ default:
46
+ return eventType;
47
+ }
48
+ };
49
+ const buildDefinedRecord = (value) => {
50
+ return Object.fromEntries(Object.entries(value).filter(([, candidate]) => candidate !== undefined));
51
+ };
52
+ const normalizeFeatureFlags = (featureFlags) => {
53
+ if (!featureFlags) {
54
+ return {};
55
+ }
56
+ return Object.fromEntries(Object.entries(featureFlags)
57
+ .map(([key, value]) => [key.trim(), (value === null || value === void 0 ? void 0 : value.trim()) || ''])
58
+ .filter(([key, value]) => Boolean(key && value)));
59
+ };
60
+ const buildFeatureFlagProperties = (featureFlags) => {
61
+ return Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [`$feature/${key}`, value]));
62
+ };
36
63
  export class PublicAnalyticsSdkService {
37
64
  constructor(options = {}) {
38
65
  this.options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options);
39
- this.endpoint = buildMainApiUrl("/sdk/public/events");
40
66
  this.eventQueue = [];
41
67
  this.flushTimerId = null;
42
68
  this.inFlightFlush = null;
43
- this.externalUserId = null;
69
+ this.userId = null;
44
70
  }
45
- getCurrentExternalUserId() {
46
- return this.ensureExternalUserId();
71
+ getCurrentUserId() {
72
+ return this.ensureUserId();
47
73
  }
48
74
  async identify(input = {}) {
49
75
  var _a;
50
- const externalUserId = persistUserId(input.externalUserId || this.ensureExternalUserId());
76
+ const userId = await this.resolveBootstrapUserId(input.user_id);
51
77
  const publishableKey = getFunnelSdkPublishableKey();
52
78
  if (!publishableKey) {
53
- this.externalUserId = externalUserId;
54
- return externalUserId;
79
+ this.userId = userId;
80
+ return userId;
55
81
  }
56
82
  try {
57
- const response = await fetch(buildMainApiUrl("/sdk/public/users/bootstrap"), {
58
- method: "POST",
83
+ const response = await fetch(buildMainApiUrl('/sdk/public/users/bootstrap'), {
84
+ method: 'POST',
59
85
  headers: buildSdkHeaders({
60
- "Content-Type": "application/json",
86
+ 'Content-Type': 'application/json',
61
87
  }),
62
88
  body: JSON.stringify({
63
89
  publishableKey,
64
90
  funnelId: FUNNEL_ID || undefined,
65
- externalUserId,
91
+ funnelVersionId: FUNNEL_VERSION_ID || undefined,
92
+ user_id: userId,
66
93
  email: asString(input.email) || undefined,
67
94
  fullName: asString(input.fullName) || undefined,
68
95
  metadata: asRecord(input.metadata),
69
96
  }),
70
97
  });
71
98
  if (!response.ok) {
72
- this.externalUserId = externalUserId;
73
- return externalUserId;
99
+ this.userId = userId;
100
+ return userId;
74
101
  }
75
102
  const payload = (await response.json());
76
- const persistedUserId = persistUserId(asString((_a = payload.user) === null || _a === void 0 ? void 0 : _a.externalUserId) ||
77
- asString(payload.externalUserId) ||
78
- externalUserId) || externalUserId;
79
- this.externalUserId = persistedUserId;
103
+ const persistedUserId = persistUserId(asString((_a = payload.user) === null || _a === void 0 ? void 0 : _a.user_id) ||
104
+ asString(payload.user_id) ||
105
+ userId, FUNNEL_ID) || userId;
106
+ this.userId = persistedUserId;
80
107
  return persistedUserId;
81
108
  }
82
109
  catch (_b) {
83
- this.externalUserId = externalUserId;
84
- return externalUserId;
110
+ this.userId = userId;
111
+ return userId;
112
+ }
113
+ }
114
+ async fetchCurrentUserState(input = {}) {
115
+ const publishableKey = getFunnelSdkPublishableKey();
116
+ if (!publishableKey || !FUNNEL_ID) {
117
+ return null;
118
+ }
119
+ const userId = asString(input.user_id) || this.getCurrentUserId();
120
+ if (!userId) {
121
+ return null;
122
+ }
123
+ try {
124
+ const url = new URL(buildMainApiUrl('/sdk/public/users/me'));
125
+ url.searchParams.set('funnelId', FUNNEL_ID);
126
+ url.searchParams.set('user_id', userId);
127
+ const response = await fetch(url.toString(), {
128
+ method: 'GET',
129
+ headers: buildSdkHeaders({}),
130
+ });
131
+ if (!response.ok) {
132
+ return null;
133
+ }
134
+ const payload = (await response.json());
135
+ return {
136
+ user: payload.user || null,
137
+ events: Array.isArray(payload.events) ? payload.events : [],
138
+ subscriptions: Array.isArray(payload.subscriptions) ? payload.subscriptions : [],
139
+ payments: Array.isArray(payload.payments) ? payload.payments : [],
140
+ };
141
+ }
142
+ catch (_a) {
143
+ return null;
85
144
  }
86
145
  }
87
146
  track(event) {
@@ -104,6 +163,7 @@ export class PublicAnalyticsSdkService {
104
163
  payload: asRecord(event.payload),
105
164
  context: asRecord(event.context),
106
165
  metadata: asRecord(event.metadata),
166
+ featureFlags: normalizeFeatureFlags(event.featureFlags),
107
167
  };
108
168
  this.eventQueue.push(envelope);
109
169
  if (this.eventQueue.length >= this.options.batchSize) {
@@ -115,22 +175,24 @@ export class PublicAnalyticsSdkService {
115
175
  }
116
176
  trackStepStarted(input) {
117
177
  return this.track({
118
- eventType: "step_start",
178
+ eventType: 'step_start',
119
179
  stepId: input.stepId,
120
180
  stepName: input.stepName,
121
181
  startedAt: input.startedAt,
122
182
  occurredAt: input.startedAt,
183
+ featureFlags: input.featureFlags,
123
184
  });
124
185
  }
125
186
  trackStepCompleted(input) {
126
187
  return this.track({
127
- eventType: "step_end",
188
+ eventType: 'step_end',
128
189
  stepId: input.stepId,
129
190
  stepName: input.stepName,
130
191
  startedAt: input.startedAt,
131
192
  endedAt: input.endedAt,
132
193
  occurredAt: input.endedAt,
133
194
  selected: asRecord(input.selected),
195
+ featureFlags: input.featureFlags,
134
196
  });
135
197
  }
136
198
  async flush() {
@@ -143,12 +205,32 @@ export class PublicAnalyticsSdkService {
143
205
  });
144
206
  return this.inFlightFlush;
145
207
  }
146
- ensureExternalUserId() {
147
- if (this.externalUserId) {
148
- return this.externalUserId;
208
+ ensureUserId() {
209
+ if (this.userId) {
210
+ return this.userId;
149
211
  }
150
- this.externalUserId = persistUserId(getOrCreateUserId());
151
- return this.externalUserId;
212
+ this.userId = persistUserId(getOrCreateUserId(FUNNEL_ID), FUNNEL_ID);
213
+ return this.userId;
214
+ }
215
+ async resolveBootstrapUserId(candidateUserId) {
216
+ var _a;
217
+ const storedUserId = readStoredUserId(FUNNEL_ID);
218
+ const fallbackUserId = storedUserId || getOrCreateUserId(FUNNEL_ID);
219
+ const normalizedCandidateUserId = asString(candidateUserId);
220
+ if (!normalizedCandidateUserId) {
221
+ return persistUserId(fallbackUserId, FUNNEL_ID);
222
+ }
223
+ if (normalizedCandidateUserId === storedUserId) {
224
+ return persistUserId(normalizedCandidateUserId, FUNNEL_ID);
225
+ }
226
+ if (!getFunnelSdkPublishableKey() || !FUNNEL_ID) {
227
+ return persistUserId(fallbackUserId, FUNNEL_ID);
228
+ }
229
+ const existingUserState = await this.fetchCurrentUserState({
230
+ user_id: normalizedCandidateUserId,
231
+ });
232
+ const validatedUserId = asString((_a = existingUserState === null || existingUserState === void 0 ? void 0 : existingUserState.user) === null || _a === void 0 ? void 0 : _a.user_id);
233
+ return persistUserId(validatedUserId || fallbackUserId, FUNNEL_ID);
152
234
  }
153
235
  scheduleFlush() {
154
236
  if (!canUseDom() || this.flushTimerId !== null || this.eventQueue.length === 0) {
@@ -186,26 +268,31 @@ export class PublicAnalyticsSdkService {
186
268
  if (events.length === 0) {
187
269
  return true;
188
270
  }
189
- const publishableKey = getFunnelSdkPublishableKey();
190
- if (!publishableKey) {
271
+ if (!getFunnelSdkPublishableKey() || !hasPostHogRuntimeConfig()) {
191
272
  return true;
192
273
  }
274
+ const batch = events.map((event) => ({
275
+ event: toPostHogEventName(event.eventType),
276
+ properties: buildDefinedRecord(Object.assign(Object.assign({ distinct_id: this.ensureUserId(), $groups: {
277
+ project: PROJECT_ID,
278
+ }, $insert_id: event.eventId, project_id: PROJECT_ID, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, step_id: event.stepId || undefined, step_name: event.stepName || undefined, event_source: 'client' }, buildFeatureFlagProperties(event.featureFlags)), { selected: Object.keys(event.selected).length > 0 ? event.selected : undefined, payload: Object.keys(event.payload).length > 0 ? event.payload : undefined, context: Object.keys(event.context).length > 0 ? event.context : undefined, metadata: Object.keys(event.metadata).length > 0 ? event.metadata : undefined })),
279
+ timestamp: event.occurredAt,
280
+ }));
193
281
  const payload = JSON.stringify({
194
- publishableKey,
195
- funnelId: FUNNEL_ID || undefined,
196
- externalUserId: this.ensureExternalUserId(),
197
- events,
282
+ api_key: POSTHOG_PROJECT_API_KEY,
283
+ batch,
198
284
  });
199
- if (canUseDom() && typeof navigator.sendBeacon === "function") {
200
- const accepted = navigator.sendBeacon(this.endpoint, new Blob([payload], { type: "application/json" }));
285
+ const endpoint = `${POSTHOG_API_HOST}${POSTHOG_BATCH_ENDPOINT}`;
286
+ if (canUseDom() && typeof navigator.sendBeacon === 'function') {
287
+ const accepted = navigator.sendBeacon(endpoint, new Blob([payload], { type: 'application/json' }));
201
288
  if (accepted) {
202
289
  return true;
203
290
  }
204
291
  }
205
292
  try {
206
- const response = await fetch(this.endpoint, {
207
- method: "POST",
208
- headers: buildSdkHeaders({ "Content-Type": "application/json" }),
293
+ const response = await fetch(endpoint, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
209
296
  body: payload,
210
297
  keepalive: true,
211
298
  });
@@ -0,0 +1 @@
1
+ export declare const readRuntimeEnv: (reader: () => string | undefined) => string | undefined;
@@ -0,0 +1,9 @@
1
+ export const readRuntimeEnv = (reader) => {
2
+ try {
3
+ const value = reader();
4
+ return typeof value === 'string' ? value : undefined;
5
+ }
6
+ catch (_a) {
7
+ return undefined;
8
+ }
9
+ };
@@ -1,7 +1,12 @@
1
1
  export declare const APP_DEALS_API_BASE_URL: string;
2
2
  export declare const FUNNEL_SDK_PUBLISHABLE_KEY: string;
3
3
  export declare const FUNNEL_ID: string;
4
+ export declare const FUNNEL_VERSION_ID: string;
5
+ export declare const PROJECT_ID: string;
6
+ export declare const POSTHOG_API_HOST = "https://us.i.posthog.com";
7
+ export declare const POSTHOG_PROJECT_API_KEY: string;
4
8
  export declare const hasRealFunnelSdkPublishableKey: (value?: string) => boolean;
5
9
  export declare const getFunnelSdkPublishableKey: () => string | null;
10
+ export declare const hasPostHogRuntimeConfig: () => boolean;
6
11
  export declare const buildMainApiUrl: (path: string) => string;
7
12
  export declare const buildSdkHeaders: (headers?: Record<string, string>) => Record<string, string>;
@@ -1,4 +1,5 @@
1
- var _a, _b, _c, _d;
1
+ var _a, _b, _c, _d, _e, _f;
2
+ import { runtimeEnvConfig } from '@funnelsgrove/runtime/config/env.config';
2
3
  const DEFAULT_API_BASE_URL = 'https://sdk-api.funnelsgrove.com';
3
4
  const DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY = 'pk_test_app_deals_seed_public';
4
5
  const trimTrailingSlash = (value) => {
@@ -18,9 +19,13 @@ const isPreviewFrameRuntime = () => {
18
19
  return false;
19
20
  }
20
21
  };
21
- export const APP_DEALS_API_BASE_URL = trimTrailingSlash((_a = process.env.NEXT_PUBLIC_APP_DEALS_API_BASE_URL) !== null && _a !== void 0 ? _a : DEFAULT_API_BASE_URL);
22
- export const FUNNEL_SDK_PUBLISHABLE_KEY = ((_c = (_b = process.env.NEXT_PUBLIC_FUNNEL_SDK_PUBLISHABLE_KEY) !== null && _b !== void 0 ? _b : process.env.NEXT_PUBLIC_FUNNEL_PUBLISHABLE_KEY) !== null && _c !== void 0 ? _c : DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY).trim();
23
- export const FUNNEL_ID = ((_d = process.env.NEXT_PUBLIC_FUNNEL_ID) !== null && _d !== void 0 ? _d : '').trim();
22
+ export const APP_DEALS_API_BASE_URL = trimTrailingSlash((_a = runtimeEnvConfig.appDealsApiBaseUrl) !== null && _a !== void 0 ? _a : DEFAULT_API_BASE_URL);
23
+ export const FUNNEL_SDK_PUBLISHABLE_KEY = ((_b = runtimeEnvConfig.funnelSdkPublishableKey) !== null && _b !== void 0 ? _b : DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY).trim();
24
+ export const FUNNEL_ID = ((_c = runtimeEnvConfig.funnelId) !== null && _c !== void 0 ? _c : '').trim();
25
+ export const FUNNEL_VERSION_ID = ((_d = runtimeEnvConfig.funnelVersionId) !== null && _d !== void 0 ? _d : '').trim();
26
+ export const PROJECT_ID = ((_e = runtimeEnvConfig.projectId) !== null && _e !== void 0 ? _e : '').trim();
27
+ export const POSTHOG_API_HOST = 'https://us.i.posthog.com';
28
+ export const POSTHOG_PROJECT_API_KEY = ((_f = runtimeEnvConfig.posthogProjectApiKey) !== null && _f !== void 0 ? _f : '').trim();
24
29
  export const hasRealFunnelSdkPublishableKey = (value = FUNNEL_SDK_PUBLISHABLE_KEY) => {
25
30
  const normalized = value.trim();
26
31
  return Boolean(normalized) && normalized !== DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY;
@@ -31,6 +36,9 @@ export const getFunnelSdkPublishableKey = () => {
31
36
  }
32
37
  return hasRealFunnelSdkPublishableKey() ? FUNNEL_SDK_PUBLISHABLE_KEY : null;
33
38
  };
39
+ export const hasPostHogRuntimeConfig = () => {
40
+ return Boolean(POSTHOG_API_HOST && POSTHOG_PROJECT_API_KEY && PROJECT_ID);
41
+ };
34
42
  export const buildMainApiUrl = (path) => {
35
43
  return `${APP_DEALS_API_BASE_URL}${toNormalizedPath(path)}`;
36
44
  };
@@ -1,7 +1,8 @@
1
1
  declare const canUseDom: () => boolean;
2
2
  declare const normalizeUserId: (value: string | null | undefined) => string | null;
3
+ declare const buildUserIdStorageKey: (funnelId?: string | null) => string;
3
4
  declare const generateUserId: () => string;
4
- declare const readStoredUserId: () => string | null;
5
- declare const persistUserId: (value: string) => string;
6
- declare const getOrCreateUserId: () => string;
7
- export { canUseDom, generateUserId, getOrCreateUserId, normalizeUserId, persistUserId, readStoredUserId, };
5
+ declare const readStoredUserId: (funnelId?: string | null) => string | null;
6
+ declare const persistUserId: (value: string, funnelId?: string | null) => string;
7
+ declare const getOrCreateUserId: (funnelId?: string | null) => string;
8
+ export { buildUserIdStorageKey, canUseDom, generateUserId, getOrCreateUserId, normalizeUserId, persistUserId, readStoredUserId, };
@@ -1,39 +1,53 @@
1
- const USER_ID_STORAGE_KEY = "funnel:external-user-id";
1
+ const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
2
2
  const canUseDom = () => {
3
- return typeof window !== "undefined";
3
+ return typeof window !== 'undefined';
4
4
  };
5
5
  const normalizeUserId = (value) => {
6
- if (!value || typeof value !== "string") {
6
+ if (!value || typeof value !== 'string') {
7
7
  return null;
8
8
  }
9
9
  const trimmed = value.trim();
10
10
  return trimmed || null;
11
11
  };
12
+ const normalizeFunnelId = (value) => {
13
+ if (!value || typeof value !== 'string') {
14
+ return null;
15
+ }
16
+ const trimmed = value.trim();
17
+ return trimmed || null;
18
+ };
19
+ const buildUserIdStorageKey = (funnelId) => {
20
+ const normalizedFunnelId = normalizeFunnelId(funnelId);
21
+ if (!normalizedFunnelId) {
22
+ return DEFAULT_USER_ID_STORAGE_KEY;
23
+ }
24
+ return `funnel:${normalizedFunnelId}:user-id`;
25
+ };
12
26
  const generateUserId = () => {
13
27
  var _a;
14
- if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === "function") {
15
- return `sdk_user_${window.crypto.randomUUID().replace(/-/g, "")}`;
28
+ if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
29
+ return `u_${window.crypto.randomUUID().replace(/-/g, '')}`;
16
30
  }
17
- return `sdk_user_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
31
+ return `u_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
18
32
  };
19
- const readStoredUserId = () => {
33
+ const readStoredUserId = (funnelId) => {
20
34
  if (!canUseDom()) {
21
35
  return null;
22
36
  }
23
- return normalizeUserId(window.localStorage.getItem(USER_ID_STORAGE_KEY));
37
+ return normalizeUserId(window.localStorage.getItem(buildUserIdStorageKey(funnelId)));
24
38
  };
25
- const persistUserId = (value) => {
39
+ const persistUserId = (value, funnelId) => {
26
40
  const normalized = normalizeUserId(value) || generateUserId();
27
41
  if (canUseDom()) {
28
- window.localStorage.setItem(USER_ID_STORAGE_KEY, normalized);
42
+ window.localStorage.setItem(buildUserIdStorageKey(funnelId), normalized);
29
43
  }
30
44
  return normalized;
31
45
  };
32
- const getOrCreateUserId = () => {
33
- const existing = readStoredUserId();
46
+ const getOrCreateUserId = (funnelId) => {
47
+ const existing = readStoredUserId(funnelId);
34
48
  if (existing) {
35
49
  return existing;
36
50
  }
37
- return persistUserId(generateUserId());
51
+ return persistUserId(generateUserId(), funnelId);
38
52
  };
39
- export { canUseDom, generateUserId, getOrCreateUserId, normalizeUserId, persistUserId, readStoredUserId, };
53
+ export { buildUserIdStorageKey, canUseDom, generateUserId, getOrCreateUserId, normalizeUserId, persistUserId, readStoredUserId, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/analytics",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -20,12 +20,14 @@
20
20
  "access": "public"
21
21
  },
22
22
  "scripts": {
23
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
23
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-esm-relative-imports.mjs dist",
24
24
  "lint": "eslint .",
25
25
  "prepublishOnly": "npm run build",
26
26
  "test:run": "vitest run --passWithNoTests"
27
27
  },
28
- "dependencies": {},
28
+ "dependencies": {
29
+ "@funnelsgrove/runtime": "0.1.1"
30
+ },
29
31
  "devDependencies": {
30
32
  "@types/node": "^20",
31
33
  "typescript": "^5",