@funnelsgrove/analytics 0.1.2 → 0.1.4

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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './services/runtime-api.config.js';
2
2
  export * from './services/sdk-user-id.storage.js';
3
3
  export * from './services/bootstrap-public-analytics-user.js';
4
+ export * from './services/meta-pixel.service.js';
4
5
  export * from './services/public-analytics-sdk.service.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './services/runtime-api.config.js';
2
2
  export * from './services/sdk-user-id.storage.js';
3
3
  export * from './services/bootstrap-public-analytics-user.js';
4
+ export * from './services/meta-pixel.service.js';
4
5
  export * from './services/public-analytics-sdk.service.js';
@@ -0,0 +1,26 @@
1
+ type MetaPixelEventName = 'AddPaymentInfo' | 'InitiateCheckout' | 'Lead' | 'ViewContent';
2
+ export type MetaPixelTrackInput = {
3
+ eventName: MetaPixelEventName;
4
+ eventId: string;
5
+ customData?: Record<string, unknown>;
6
+ };
7
+ export type AnalyticsEventForMetaPixel = {
8
+ eventType: string;
9
+ eventId: string;
10
+ metadata?: Record<string, unknown>;
11
+ };
12
+ declare global {
13
+ interface Window {
14
+ fbq?: (...args: unknown[]) => void;
15
+ }
16
+ }
17
+ export declare const mapAnalyticsEventToMetaPixelEvent: (event: AnalyticsEventForMetaPixel) => MetaPixelTrackInput | null;
18
+ export declare class MetaPixelService {
19
+ private initialized;
20
+ track(input: MetaPixelTrackInput): void;
21
+ private isConfigured;
22
+ private ensureInitialized;
23
+ private installScript;
24
+ }
25
+ export declare const metaPixelService: MetaPixelService;
26
+ export {};
@@ -0,0 +1,100 @@
1
+ import { META_PIXEL_ENABLED, META_PIXEL_ID } from './runtime-api.config.js';
2
+ const toNumber = (value) => {
3
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
4
+ return null;
5
+ }
6
+ return value;
7
+ };
8
+ const toCurrency = (value) => {
9
+ return typeof value === 'string' && value.trim() ? value.trim().toUpperCase() : undefined;
10
+ };
11
+ const buildCommerceCustomData = (metadata = {}) => {
12
+ var _a;
13
+ const amountCents = toNumber(metadata.amountCents);
14
+ const value = (_a = toNumber(metadata.value)) !== null && _a !== void 0 ? _a : (amountCents === null ? null : amountCents / 100);
15
+ const currency = toCurrency(metadata.currency);
16
+ const customData = {};
17
+ if (currency) {
18
+ customData.currency = currency;
19
+ }
20
+ if (value !== null) {
21
+ customData.value = value;
22
+ }
23
+ return customData;
24
+ };
25
+ export const mapAnalyticsEventToMetaPixelEvent = (event) => {
26
+ switch (event.eventType) {
27
+ case 'first_step_viewed':
28
+ case 'first_step_clicked':
29
+ return {
30
+ eventName: 'ViewContent',
31
+ eventId: event.eventId,
32
+ customData: {},
33
+ };
34
+ case 'email_captured':
35
+ case 'lead':
36
+ return {
37
+ eventName: 'Lead',
38
+ eventId: event.eventId,
39
+ customData: {},
40
+ };
41
+ case 'checkout_started':
42
+ return {
43
+ eventName: 'InitiateCheckout',
44
+ eventId: event.eventId,
45
+ customData: buildCommerceCustomData(event.metadata),
46
+ };
47
+ case 'checkout_completed':
48
+ case 'payment_checkout_succeeded':
49
+ return {
50
+ eventName: 'AddPaymentInfo',
51
+ eventId: event.eventId,
52
+ customData: buildCommerceCustomData(event.metadata),
53
+ };
54
+ default:
55
+ return null;
56
+ }
57
+ };
58
+ export class MetaPixelService {
59
+ constructor() {
60
+ this.initialized = false;
61
+ }
62
+ track(input) {
63
+ var _a, _b;
64
+ if (!this.isConfigured()) {
65
+ return;
66
+ }
67
+ this.ensureInitialized();
68
+ (_a = window.fbq) === null || _a === void 0 ? void 0 : _a.call(window, 'track', input.eventName, (_b = input.customData) !== null && _b !== void 0 ? _b : {}, {
69
+ eventID: input.eventId,
70
+ });
71
+ }
72
+ isConfigured() {
73
+ return Boolean(META_PIXEL_ENABLED && META_PIXEL_ID && typeof window !== 'undefined');
74
+ }
75
+ ensureInitialized() {
76
+ var _a, _b;
77
+ if (this.initialized) {
78
+ return;
79
+ }
80
+ this.initialized = true;
81
+ this.installScript();
82
+ (_a = window.fbq) === null || _a === void 0 ? void 0 : _a.call(window, 'init', META_PIXEL_ID);
83
+ (_b = window.fbq) === null || _b === void 0 ? void 0 : _b.call(window, 'track', 'PageView');
84
+ }
85
+ installScript() {
86
+ if (typeof document === 'undefined') {
87
+ return;
88
+ }
89
+ const firstScript = document.getElementsByTagName('script')[0];
90
+ const parent = firstScript === null || firstScript === void 0 ? void 0 : firstScript.parentNode;
91
+ if (!parent) {
92
+ return;
93
+ }
94
+ const script = document.createElement('script');
95
+ script.async = true;
96
+ script.src = 'https://connect.facebook.net/en_US/fbevents.js';
97
+ parent.insertBefore(script, firstScript);
98
+ }
99
+ }
100
+ export const metaPixelService = new MetaPixelService();
@@ -26,6 +26,23 @@ export type PublicSdkStepEndInput = {
26
26
  selected?: Record<string, unknown>;
27
27
  featureFlags?: Record<string, string | null | undefined>;
28
28
  };
29
+ export type PublicSdkStepEngagedInput = {
30
+ stepId: string;
31
+ stepName: string;
32
+ startedAt: string;
33
+ engagedAt: string;
34
+ durationMs: number;
35
+ engagementThresholdMs: number;
36
+ featureFlags?: Record<string, string | null | undefined>;
37
+ };
38
+ export type PublicSdkConversionCallbackInput = {
39
+ eventId?: string;
40
+ occurredAt?: string;
41
+ stepId?: string;
42
+ stepName?: string;
43
+ metadata?: Record<string, unknown>;
44
+ featureFlags?: Record<string, string | null | undefined>;
45
+ };
29
46
  export type PublicSdkIdentifyInput = {
30
47
  user_id?: string;
31
48
  email?: string;
@@ -56,6 +73,12 @@ export declare class PublicAnalyticsSdkService {
56
73
  track(event: PublicSdkTrackEventInput): string | null;
57
74
  trackStepStarted(input: PublicSdkStepStartInput): string | null;
58
75
  trackStepCompleted(input: PublicSdkStepEndInput): string | null;
76
+ trackStepEngaged(input: PublicSdkStepEngagedInput): string | null;
77
+ trackFirstStepClicked(input: PublicSdkConversionCallbackInput): string | null;
78
+ trackFirstStepViewed(input: PublicSdkConversionCallbackInput): string | null;
79
+ trackLead(input: PublicSdkConversionCallbackInput): string | null;
80
+ trackCheckoutStarted(input: PublicSdkConversionCallbackInput): string | null;
81
+ trackCheckoutCompleted(input: PublicSdkConversionCallbackInput): string | null;
59
82
  flush(): Promise<number>;
60
83
  private ensureUserId;
61
84
  private resolveBootstrapUserId;
@@ -1,5 +1,6 @@
1
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
2
  import { canUseDom, getOrCreateUserId, persistUserId, readStoredUserId, } from './sdk-user-id.storage.js';
3
+ import { mapAnalyticsEventToMetaPixelEvent, metaPixelService, } from './meta-pixel.service.js';
3
4
  const DEFAULT_OPTIONS = {
4
5
  batchSize: 20,
5
6
  flushIntervalMs: 1500,
@@ -27,6 +28,23 @@ const toIsoOrNow = (value) => {
27
28
  }
28
29
  return new Date().toISOString();
29
30
  };
31
+ const toFiniteNumber = (value) => {
32
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
33
+ return null;
34
+ }
35
+ return value;
36
+ };
37
+ const toDurationMs = (startedAt, endedAt) => {
38
+ if (!startedAt || !endedAt) {
39
+ return null;
40
+ }
41
+ const startedMs = new Date(startedAt).getTime();
42
+ const endedMs = new Date(endedAt).getTime();
43
+ if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs) || endedMs < startedMs) {
44
+ return null;
45
+ }
46
+ return endedMs - startedMs;
47
+ };
30
48
  const buildEventId = () => {
31
49
  var _a;
32
50
  if (canUseDom() && typeof ((_a = window.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === 'function') {
@@ -144,21 +162,33 @@ export class PublicAnalyticsSdkService {
144
162
  }
145
163
  }
146
164
  track(event) {
165
+ var _a, _b, _c, _d;
147
166
  const eventType = asString(event.eventType);
148
167
  if (!eventType) {
149
168
  return null;
150
169
  }
170
+ const eventId = asString(event.eventId) || buildEventId();
171
+ const metaPixelEvent = mapAnalyticsEventToMetaPixelEvent({
172
+ eventType,
173
+ eventId,
174
+ metadata: asRecord(event.metadata),
175
+ });
176
+ if (metaPixelEvent) {
177
+ metaPixelService.track(metaPixelEvent);
178
+ }
151
179
  if (!getFunnelSdkPublishableKey()) {
152
- return asString(event.eventId) || buildEventId();
180
+ return eventId;
153
181
  }
154
182
  const envelope = {
155
183
  eventType,
156
- eventId: asString(event.eventId) || buildEventId(),
184
+ eventId,
157
185
  occurredAt: toIsoOrNow(event.occurredAt),
158
186
  stepId: asString(event.stepId),
159
187
  stepName: asString(event.stepName),
160
188
  startedAt: asString(event.startedAt),
161
189
  endedAt: asString(event.endedAt),
190
+ durationMs: (_b = toFiniteNumber((_a = event.metadata) === null || _a === void 0 ? void 0 : _a.durationMs)) !== null && _b !== void 0 ? _b : null,
191
+ engagementThresholdMs: (_d = toFiniteNumber((_c = event.metadata) === null || _c === void 0 ? void 0 : _c.engagementThresholdMs)) !== null && _d !== void 0 ? _d : null,
162
192
  selected: asRecord(event.selected),
163
193
  payload: asRecord(event.payload),
164
194
  context: asRecord(event.context),
@@ -184,14 +214,90 @@ export class PublicAnalyticsSdkService {
184
214
  });
185
215
  }
186
216
  trackStepCompleted(input) {
217
+ var _a;
218
+ const startedAt = asString(input.startedAt);
219
+ const endedAt = asString(input.endedAt);
187
220
  return this.track({
188
221
  eventType: 'step_end',
189
222
  stepId: input.stepId,
190
223
  stepName: input.stepName,
191
- startedAt: input.startedAt,
192
- endedAt: input.endedAt,
224
+ startedAt: startedAt || input.startedAt,
225
+ endedAt: endedAt || input.endedAt,
193
226
  occurredAt: input.endedAt,
194
227
  selected: asRecord(input.selected),
228
+ metadata: {
229
+ durationMs: (_a = toDurationMs(startedAt, endedAt)) !== null && _a !== void 0 ? _a : undefined,
230
+ },
231
+ featureFlags: input.featureFlags,
232
+ });
233
+ }
234
+ trackStepEngaged(input) {
235
+ return this.track({
236
+ eventType: 'step_engaged',
237
+ stepId: input.stepId,
238
+ stepName: input.stepName,
239
+ startedAt: input.startedAt,
240
+ endedAt: input.engagedAt,
241
+ occurredAt: input.engagedAt,
242
+ metadata: {
243
+ durationMs: input.durationMs,
244
+ engagementThresholdMs: input.engagementThresholdMs,
245
+ },
246
+ featureFlags: input.featureFlags,
247
+ });
248
+ }
249
+ trackFirstStepClicked(input) {
250
+ return this.track({
251
+ eventType: 'first_step_clicked',
252
+ eventId: input.eventId,
253
+ occurredAt: input.occurredAt,
254
+ stepId: input.stepId,
255
+ stepName: input.stepName,
256
+ metadata: input.metadata,
257
+ featureFlags: input.featureFlags,
258
+ });
259
+ }
260
+ trackFirstStepViewed(input) {
261
+ return this.track({
262
+ eventType: 'first_step_viewed',
263
+ eventId: input.eventId,
264
+ occurredAt: input.occurredAt,
265
+ stepId: input.stepId,
266
+ stepName: input.stepName,
267
+ metadata: input.metadata,
268
+ featureFlags: input.featureFlags,
269
+ });
270
+ }
271
+ trackLead(input) {
272
+ return this.track({
273
+ eventType: 'email_captured',
274
+ eventId: input.eventId,
275
+ occurredAt: input.occurredAt,
276
+ stepId: input.stepId,
277
+ stepName: input.stepName,
278
+ metadata: input.metadata,
279
+ featureFlags: input.featureFlags,
280
+ });
281
+ }
282
+ trackCheckoutStarted(input) {
283
+ return this.track({
284
+ eventType: 'checkout_started',
285
+ eventId: input.eventId,
286
+ occurredAt: input.occurredAt,
287
+ stepId: input.stepId,
288
+ stepName: input.stepName,
289
+ metadata: input.metadata,
290
+ featureFlags: input.featureFlags,
291
+ });
292
+ }
293
+ trackCheckoutCompleted(input) {
294
+ return this.track({
295
+ eventType: 'checkout_completed',
296
+ eventId: input.eventId,
297
+ occurredAt: input.occurredAt,
298
+ stepId: input.stepId,
299
+ stepName: input.stepName,
300
+ metadata: input.metadata,
195
301
  featureFlags: input.featureFlags,
196
302
  });
197
303
  }
@@ -271,13 +377,16 @@ export class PublicAnalyticsSdkService {
271
377
  if (!getFunnelSdkPublishableKey() || !hasPostHogRuntimeConfig()) {
272
378
  return true;
273
379
  }
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
- }));
380
+ const batch = events.map((event) => {
381
+ var _a, _b;
382
+ return ({
383
+ event: toPostHogEventName(event.eventType),
384
+ properties: buildDefinedRecord(Object.assign(Object.assign({ distinct_id: this.ensureUserId(), $groups: {
385
+ project: PROJECT_ID,
386
+ }, $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, started_at: event.startedAt || undefined, ended_at: event.endedAt || undefined, duration_ms: (_a = event.durationMs) !== null && _a !== void 0 ? _a : undefined, engagement_threshold_ms: (_b = event.engagementThresholdMs) !== null && _b !== void 0 ? _b : 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 })),
387
+ timestamp: event.occurredAt,
388
+ });
389
+ });
281
390
  const payload = JSON.stringify({
282
391
  api_key: POSTHOG_PROJECT_API_KEY,
283
392
  batch,
@@ -5,6 +5,8 @@ export declare const FUNNEL_VERSION_ID: string;
5
5
  export declare const PROJECT_ID: string;
6
6
  export declare const POSTHOG_API_HOST = "https://us.i.posthog.com";
7
7
  export declare const POSTHOG_PROJECT_API_KEY: string;
8
+ export declare const META_PIXEL_ENABLED: boolean;
9
+ export declare const META_PIXEL_ID: string;
8
10
  export declare const hasRealFunnelSdkPublishableKey: (value?: string) => boolean;
9
11
  export declare const getFunnelSdkPublishableKey: () => string | null;
10
12
  export declare const hasPostHogRuntimeConfig: () => boolean;
@@ -1,5 +1,5 @@
1
- var _a, _b, _c, _d, _e, _f;
2
- import { runtimeEnvConfig } from '@funnelsgrove/runtime/config/env.config';
1
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2
+ import { resolveRuntimeSdkApiBaseUrl, runtimeEnvConfig, } from '@funnelsgrove/runtime/config/env.config';
3
3
  const DEFAULT_API_BASE_URL = 'https://sdk-api.funnelsgrove.com';
4
4
  const DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY = 'pk_test_app_deals_seed_public';
5
5
  const trimTrailingSlash = (value) => {
@@ -19,13 +19,15 @@ const isPreviewFrameRuntime = () => {
19
19
  return false;
20
20
  }
21
21
  };
22
- export const APP_DEALS_API_BASE_URL = trimTrailingSlash((_a = runtimeEnvConfig.appDealsApiBaseUrl) !== null && _a !== void 0 ? _a : DEFAULT_API_BASE_URL);
22
+ export const APP_DEALS_API_BASE_URL = trimTrailingSlash(resolveRuntimeSdkApiBaseUrl((_a = runtimeEnvConfig.appDealsApiBaseUrl) !== null && _a !== void 0 ? _a : DEFAULT_API_BASE_URL));
23
23
  export const FUNNEL_SDK_PUBLISHABLE_KEY = ((_b = runtimeEnvConfig.funnelSdkPublishableKey) !== null && _b !== void 0 ? _b : DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY).trim();
24
24
  export const FUNNEL_ID = ((_c = runtimeEnvConfig.funnelId) !== null && _c !== void 0 ? _c : '').trim();
25
25
  export const FUNNEL_VERSION_ID = ((_d = runtimeEnvConfig.funnelVersionId) !== null && _d !== void 0 ? _d : '').trim();
26
26
  export const PROJECT_ID = ((_e = runtimeEnvConfig.projectId) !== null && _e !== void 0 ? _e : '').trim();
27
27
  export const POSTHOG_API_HOST = 'https://us.i.posthog.com';
28
28
  export const POSTHOG_PROJECT_API_KEY = ((_f = runtimeEnvConfig.posthogProjectApiKey) !== null && _f !== void 0 ? _f : '').trim();
29
+ export const META_PIXEL_ENABLED = ((_g = runtimeEnvConfig.metaPixelEnabled) !== null && _g !== void 0 ? _g : '').trim() === 'true';
30
+ export const META_PIXEL_ID = ((_h = runtimeEnvConfig.metaPixelId) !== null && _h !== void 0 ? _h : '').trim();
29
31
  export const hasRealFunnelSdkPublishableKey = (value = FUNNEL_SDK_PUBLISHABLE_KEY) => {
30
32
  const normalized = value.trim();
31
33
  return Boolean(normalized) && normalized !== DEFAULT_FUNNEL_SDK_PUBLISHABLE_KEY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/analytics",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test:run": "vitest run --passWithNoTests"
27
27
  },
28
28
  "dependencies": {
29
- "@funnelsgrove/runtime": "0.1.2"
29
+ "@funnelsgrove/runtime": "0.1.3"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^20",