@cognior/iap-sdk 0.1.0
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.
Potentially problematic release.
This version of @cognior/iap-sdk might be problematic. Click here for more details.
- package/.github/copilot-instructions.md +95 -0
- package/README.md +79 -0
- package/TRACKING.md +105 -0
- package/USER_CONTEXT_README.md +284 -0
- package/package.json +154 -0
- package/src/config.ts +25 -0
- package/src/core/flowEngine.ts +1833 -0
- package/src/core/triggerManager.ts +1011 -0
- package/src/experiences/banner.ts +366 -0
- package/src/experiences/beacon.ts +668 -0
- package/src/experiences/hotspotTour.ts +654 -0
- package/src/experiences/hotspots.ts +566 -0
- package/src/experiences/modal.ts +1337 -0
- package/src/experiences/modalSequence.ts +1247 -0
- package/src/experiences/popover.ts +652 -0
- package/src/experiences/registry.ts +21 -0
- package/src/experiences/survey.ts +1639 -0
- package/src/experiences/taskList.ts +625 -0
- package/src/experiences/tooltip.ts +740 -0
- package/src/experiences/types.ts +395 -0
- package/src/experiences/walkthrough.ts +670 -0
- package/src/flow-sequence.ts +177 -0
- package/src/flows.ts +512 -0
- package/src/http.ts +61 -0
- package/src/index.ts +355 -0
- package/src/services/flowManager.ts +905 -0
- package/src/services/flowNormalizer.ts +74 -0
- package/src/services/locationContextService.ts +189 -0
- package/src/services/pageContextService.ts +221 -0
- package/src/services/userContextService.ts +286 -0
- package/src/state/appState.ts +0 -0
- package/src/state/hooks.ts +0 -0
- package/src/state/index.ts +0 -0
- package/src/state/migration.ts +0 -0
- package/src/state/store.ts +0 -0
- package/src/styles/banner.css.ts +0 -0
- package/src/styles/hotspot.css.ts +0 -0
- package/src/styles/hotspotTour.css.ts +0 -0
- package/src/styles/modal.css.ts +564 -0
- package/src/styles/survey.css.ts +1013 -0
- package/src/styles/taskList.css.ts +0 -0
- package/src/styles/tooltip.css.ts +149 -0
- package/src/styles/walkthrough.css.ts +0 -0
- package/src/tourUtils.ts +0 -0
- package/src/tracking.ts +223 -0
- package/src/utils/debounce.ts +66 -0
- package/src/utils/eventSequenceValidator.ts +124 -0
- package/src/utils/flowTrackingSystem.ts +524 -0
- package/src/utils/idGenerator.ts +155 -0
- package/src/utils/immediateValidationPrevention.ts +184 -0
- package/src/utils/normalize.ts +50 -0
- package/src/utils/privacyManager.ts +166 -0
- package/src/utils/ruleEvaluator.ts +199 -0
- package/src/utils/sanitize.ts +79 -0
- package/src/utils/selectors.ts +107 -0
- package/src/utils/stepExecutor.ts +345 -0
- package/src/utils/triggerNormalizer.ts +149 -0
- package/src/utils/validationInterceptor.ts +650 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// src/utils/flowTrackingSystem.ts
|
|
2
|
+
// A comprehensive tracking system for flows that ensures consistent
|
|
3
|
+
// tracking aligned with actual user flow navigation
|
|
4
|
+
|
|
5
|
+
import { DapConfig } from '../config';
|
|
6
|
+
|
|
7
|
+
// Define the core flow states
|
|
8
|
+
export enum FlowState {
|
|
9
|
+
NOT_STARTED = 'not_started',
|
|
10
|
+
STARTED = 'started',
|
|
11
|
+
NAVIGATING = 'navigating',
|
|
12
|
+
COMPLETED = 'completed',
|
|
13
|
+
EXITED = 'exited'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Define the core step states
|
|
17
|
+
export enum StepState {
|
|
18
|
+
NOT_VISITED = 'not_visited',
|
|
19
|
+
ACTIVE = 'active',
|
|
20
|
+
COMPLETED = 'completed',
|
|
21
|
+
SKIPPED = 'skipped'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Define an action type for tracking
|
|
25
|
+
export enum TrackingAction {
|
|
26
|
+
FLOW_IMPRESSION = 'flow_impression', // Flow is visible/loaded
|
|
27
|
+
FLOW_STARTED = 'flow_started', // User actively started the flow (formerly flow_initialized/flow_initiation)
|
|
28
|
+
FLOW_INTERACTION = 'flow_interaction', // User interacted with flow elements
|
|
29
|
+
STEP_TRANSITION = 'step_transition', // User navigated between steps
|
|
30
|
+
STEP_COMPLETION = 'step_completion', // User completed a step in the flow
|
|
31
|
+
FLOW_COMPLETED = 'flow_completed', // User completed the flow (formerly flow_completion)
|
|
32
|
+
FLOW_EXITED = 'flow_exited', // User exited without completion (formerly flow_exit)
|
|
33
|
+
FLOW_REENTRY = 'flow_reentry', // User returned to a previously started flow
|
|
34
|
+
FLOW_SUBMISSION = 'flow_submission', // User submitted a form within the flow
|
|
35
|
+
FLOW_INPUT_INTERACTION = 'flow_input_interaction', // User interacted with an input field
|
|
36
|
+
FLOW_TRANSITION = 'flow_transition', // User transitioned between flows
|
|
37
|
+
CUSTOM_ACTION = 'custom_action' // Special user actions like downloads or video plays
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Interface for tracking data
|
|
41
|
+
export interface FlowTrackingInfo {
|
|
42
|
+
flowId: string;
|
|
43
|
+
state: FlowState;
|
|
44
|
+
startTime: number | null;
|
|
45
|
+
lastInteractionTime: number | null;
|
|
46
|
+
steps: Record<string, {
|
|
47
|
+
id: string;
|
|
48
|
+
state: StepState;
|
|
49
|
+
timeSpent: number;
|
|
50
|
+
interactions: number;
|
|
51
|
+
firstVisitTime: number | null;
|
|
52
|
+
lastVisitTime: number | null;
|
|
53
|
+
}>;
|
|
54
|
+
currentStepId: string | null;
|
|
55
|
+
previousStepId: string | null;
|
|
56
|
+
completedSteps: string[];
|
|
57
|
+
totalSteps: number;
|
|
58
|
+
completionPercentage: number;
|
|
59
|
+
isTransitioning: boolean;
|
|
60
|
+
targetFlowId: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The global registry of flow tracking information
|
|
64
|
+
const flowRegistry = new Map<string, FlowTrackingInfo>();
|
|
65
|
+
|
|
66
|
+
// Track impressions that have been sent in the current session
|
|
67
|
+
const sentImpressions = new Set<string>();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initialize tracking for a flow
|
|
71
|
+
* @param flowId The ID of the flow to track
|
|
72
|
+
* @returns The flow tracking information
|
|
73
|
+
*/
|
|
74
|
+
export function initializeFlowTracking(flowId: string): FlowTrackingInfo {
|
|
75
|
+
// Check if we already have tracking for this flow
|
|
76
|
+
const existingTracking = flowRegistry.get(flowId);
|
|
77
|
+
if (existingTracking) {
|
|
78
|
+
console.debug(`[DAP] Flow tracking already initialized for flow ${flowId}`);
|
|
79
|
+
return existingTracking;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.debug(`[DAP] Initializing flow tracking for flow ${flowId}`);
|
|
83
|
+
|
|
84
|
+
// Try to restore tracking from session storage
|
|
85
|
+
const restoredTracking = restoreFlowTracking(flowId);
|
|
86
|
+
if (restoredTracking) {
|
|
87
|
+
flowRegistry.set(flowId, restoredTracking);
|
|
88
|
+
console.debug(`[DAP] Restored flow tracking from session storage for flow ${flowId}`);
|
|
89
|
+
return restoredTracking;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create new tracking if not found
|
|
93
|
+
const newTracking: FlowTrackingInfo = {
|
|
94
|
+
flowId,
|
|
95
|
+
state: FlowState.NOT_STARTED,
|
|
96
|
+
startTime: null,
|
|
97
|
+
lastInteractionTime: null,
|
|
98
|
+
steps: {},
|
|
99
|
+
currentStepId: null,
|
|
100
|
+
previousStepId: null,
|
|
101
|
+
completedSteps: [],
|
|
102
|
+
totalSteps: 0,
|
|
103
|
+
completionPercentage: 0,
|
|
104
|
+
isTransitioning: false,
|
|
105
|
+
targetFlowId: null
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
flowRegistry.set(flowId, newTracking);
|
|
109
|
+
console.debug(`[DAP] Created new flow tracking for flow ${flowId}`);
|
|
110
|
+
|
|
111
|
+
// Persist to session storage
|
|
112
|
+
persistFlowTracking(flowId);
|
|
113
|
+
|
|
114
|
+
return newTracking;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get tracking information for a flow
|
|
119
|
+
* @param flowId The ID of the flow
|
|
120
|
+
* @returns The flow tracking information, or null if not found
|
|
121
|
+
*/
|
|
122
|
+
export function getFlowTracking(flowId: string): FlowTrackingInfo | null {
|
|
123
|
+
return flowRegistry.get(flowId) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mark a flow as started
|
|
128
|
+
* @param config The DAP configuration
|
|
129
|
+
* @param flowId The ID of the flow
|
|
130
|
+
* @param stepId The ID of the starting step
|
|
131
|
+
*/
|
|
132
|
+
export function startFlow(config: DapConfig, flowId: string, stepId: string): void {
|
|
133
|
+
const tracking = initializeFlowTracking(flowId);
|
|
134
|
+
|
|
135
|
+
// Only track if the flow hasn't been started
|
|
136
|
+
if (tracking.state === FlowState.NOT_STARTED) {
|
|
137
|
+
tracking.state = FlowState.STARTED;
|
|
138
|
+
tracking.startTime = Date.now();
|
|
139
|
+
tracking.currentStepId = stepId;
|
|
140
|
+
|
|
141
|
+
// Initialize the step if it doesn't exist
|
|
142
|
+
ensureStepExists(tracking, stepId);
|
|
143
|
+
|
|
144
|
+
// Mark the step as active
|
|
145
|
+
tracking.steps[stepId].state = StepState.ACTIVE;
|
|
146
|
+
tracking.steps[stepId].firstVisitTime = Date.now();
|
|
147
|
+
tracking.steps[stepId].lastVisitTime = Date.now();
|
|
148
|
+
|
|
149
|
+
// Persist changes
|
|
150
|
+
persistFlowTracking(flowId);
|
|
151
|
+
|
|
152
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
153
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow ${flowId} started with step ${stepId}`);
|
|
154
|
+
|
|
155
|
+
console.debug(`[DAP] Flow ${flowId} started with step ${stepId}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Track a step transition within a flow
|
|
161
|
+
* @param config The DAP configuration
|
|
162
|
+
* @param flowId The ID of the flow
|
|
163
|
+
* @param fromStepId The ID of the previous step
|
|
164
|
+
* @param toStepId The ID of the new step
|
|
165
|
+
*/
|
|
166
|
+
export function trackStepTransition(
|
|
167
|
+
config: DapConfig,
|
|
168
|
+
flowId: string,
|
|
169
|
+
fromStepId: string,
|
|
170
|
+
toStepId: string
|
|
171
|
+
): void {
|
|
172
|
+
const tracking = getFlowTracking(flowId);
|
|
173
|
+
if (!tracking) return;
|
|
174
|
+
|
|
175
|
+
// Skip if flow is completed or exited
|
|
176
|
+
if (tracking.state === FlowState.COMPLETED || tracking.state === FlowState.EXITED) {
|
|
177
|
+
console.debug(`[DAP] Ignoring step transition for ${flowId}: flow already in state ${tracking.state}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update flow state to navigating
|
|
182
|
+
tracking.state = FlowState.NAVIGATING;
|
|
183
|
+
tracking.lastInteractionTime = Date.now();
|
|
184
|
+
|
|
185
|
+
// Ensure from step exists and update it
|
|
186
|
+
ensureStepExists(tracking, fromStepId);
|
|
187
|
+
tracking.steps[fromStepId].state = StepState.COMPLETED;
|
|
188
|
+
|
|
189
|
+
// Calculate time spent on previous step
|
|
190
|
+
if (tracking.steps[fromStepId].lastVisitTime !== null) {
|
|
191
|
+
tracking.steps[fromStepId].timeSpent +=
|
|
192
|
+
Date.now() - tracking.steps[fromStepId].lastVisitTime;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ensure to step exists and update it
|
|
196
|
+
ensureStepExists(tracking, toStepId);
|
|
197
|
+
tracking.steps[toStepId].state = StepState.ACTIVE;
|
|
198
|
+
tracking.steps[toStepId].lastVisitTime = Date.now();
|
|
199
|
+
|
|
200
|
+
// If this is the first visit, set first visit time
|
|
201
|
+
if (tracking.steps[toStepId].firstVisitTime === null) {
|
|
202
|
+
tracking.steps[toStepId].firstVisitTime = Date.now();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Update current and previous step
|
|
206
|
+
tracking.previousStepId = fromStepId;
|
|
207
|
+
tracking.currentStepId = toStepId;
|
|
208
|
+
|
|
209
|
+
// Add completed step if not already there
|
|
210
|
+
if (!tracking.completedSteps.includes(fromStepId)) {
|
|
211
|
+
tracking.completedSteps.push(fromStepId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update completion percentage
|
|
215
|
+
tracking.completionPercentage = calculateCompletionPercentage(tracking);
|
|
216
|
+
|
|
217
|
+
// Persist changes
|
|
218
|
+
persistFlowTracking(flowId);
|
|
219
|
+
|
|
220
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
221
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow ${flowId} transition from ${fromStepId} to ${toStepId}`);
|
|
222
|
+
|
|
223
|
+
console.debug(`[DAP] Flow ${flowId} transition from ${fromStepId} to ${toStepId}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Track user interaction with a flow element
|
|
228
|
+
* @param config The DAP configuration
|
|
229
|
+
* @param flowId The ID of the flow
|
|
230
|
+
* @param stepId The ID of the step
|
|
231
|
+
* @param selector The CSS selector for the element
|
|
232
|
+
* @param context Additional context
|
|
233
|
+
*/
|
|
234
|
+
export function trackInteraction(
|
|
235
|
+
config: DapConfig,
|
|
236
|
+
flowId: string,
|
|
237
|
+
stepId: string,
|
|
238
|
+
selector?: string,
|
|
239
|
+
context?: string
|
|
240
|
+
): void {
|
|
241
|
+
const tracking = getFlowTracking(flowId);
|
|
242
|
+
if (!tracking) return;
|
|
243
|
+
|
|
244
|
+
// Skip if already completed
|
|
245
|
+
if (tracking.state === FlowState.COMPLETED || tracking.state === FlowState.EXITED) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Ensure step exists
|
|
250
|
+
ensureStepExists(tracking, stepId);
|
|
251
|
+
|
|
252
|
+
// Update interaction count
|
|
253
|
+
tracking.steps[stepId].interactions++;
|
|
254
|
+
tracking.lastInteractionTime = Date.now();
|
|
255
|
+
|
|
256
|
+
// If flow not yet started, mark as started
|
|
257
|
+
if (tracking.state === FlowState.NOT_STARTED) {
|
|
258
|
+
startFlow(config, flowId, stepId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
262
|
+
if (selector) {
|
|
263
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow ${flowId} interaction on step ${stepId}:`, selector);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Persist changes
|
|
267
|
+
persistFlowTracking(flowId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Track impression of a flow
|
|
272
|
+
* @param config The DAP configuration
|
|
273
|
+
* @param flowId The ID of the flow
|
|
274
|
+
* @returns True if impression was tracked, false if already tracked
|
|
275
|
+
*/
|
|
276
|
+
export function trackImpression(config: DapConfig, flowId: string): boolean {
|
|
277
|
+
// Initialize tracking
|
|
278
|
+
const flowTracking = initializeFlowTracking(flowId);
|
|
279
|
+
|
|
280
|
+
// Don't track impression again if flow is already started or beyond
|
|
281
|
+
if (flowTracking.state !== FlowState.NOT_STARTED) {
|
|
282
|
+
console.debug(`[DAP] Skipping impression tracking for flow ${flowId} - already in state ${flowTracking.state}`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Only send impression once per page load
|
|
287
|
+
if (sentImpressions.has(flowId)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Mark as sent
|
|
292
|
+
sentImpressions.add(flowId);
|
|
293
|
+
|
|
294
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
295
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow impression for ${flowId}`);
|
|
296
|
+
|
|
297
|
+
console.debug(`[DAP] Flow impression tracked for ${flowId}`);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Mark a flow as completed
|
|
303
|
+
* @param config The DAP configuration
|
|
304
|
+
* @param flowId The ID of the flow
|
|
305
|
+
* @param finalStepId The ID of the final step (optional)
|
|
306
|
+
*/
|
|
307
|
+
export function completeFlow(
|
|
308
|
+
config: DapConfig,
|
|
309
|
+
flowId: string,
|
|
310
|
+
finalStepId?: string
|
|
311
|
+
): void {
|
|
312
|
+
const tracking = getFlowTracking(flowId);
|
|
313
|
+
if (!tracking) return;
|
|
314
|
+
|
|
315
|
+
// Skip if already completed
|
|
316
|
+
if (tracking.state === FlowState.COMPLETED) {
|
|
317
|
+
console.debug(`[DAP] Flow ${flowId} already marked as completed`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Use provided final step or current step
|
|
322
|
+
const effectiveStepId = finalStepId || tracking.currentStepId;
|
|
323
|
+
if (!effectiveStepId) return;
|
|
324
|
+
|
|
325
|
+
// Update flow state
|
|
326
|
+
tracking.state = FlowState.COMPLETED;
|
|
327
|
+
tracking.lastInteractionTime = Date.now();
|
|
328
|
+
|
|
329
|
+
// Ensure the step exists
|
|
330
|
+
ensureStepExists(tracking, effectiveStepId);
|
|
331
|
+
|
|
332
|
+
// Mark the final step as completed
|
|
333
|
+
tracking.steps[effectiveStepId].state = StepState.COMPLETED;
|
|
334
|
+
tracking.steps[effectiveStepId].lastVisitTime = Date.now();
|
|
335
|
+
|
|
336
|
+
// Update completion data
|
|
337
|
+
if (!tracking.completedSteps.includes(effectiveStepId)) {
|
|
338
|
+
tracking.completedSteps.push(effectiveStepId);
|
|
339
|
+
}
|
|
340
|
+
tracking.completionPercentage = 100;
|
|
341
|
+
|
|
342
|
+
// Persist changes
|
|
343
|
+
persistFlowTracking(flowId);
|
|
344
|
+
|
|
345
|
+
// Always send completion event
|
|
346
|
+
const totalTime = tracking.startTime ? Date.now() - tracking.startTime : 0;
|
|
347
|
+
|
|
348
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
349
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow ${flowId} completed on step ${effectiveStepId}`);
|
|
350
|
+
|
|
351
|
+
console.debug(`[DAP] Flow ${flowId} completed on step ${effectiveStepId}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Track flow exit
|
|
356
|
+
* @param config The DAP configuration
|
|
357
|
+
* @param flowId The ID of the flow
|
|
358
|
+
* @param reason The reason for exiting
|
|
359
|
+
*/
|
|
360
|
+
export function exitFlow(
|
|
361
|
+
config: DapConfig,
|
|
362
|
+
flowId: string,
|
|
363
|
+
reason: string
|
|
364
|
+
): void {
|
|
365
|
+
const tracking = getFlowTracking(flowId);
|
|
366
|
+
if (!tracking) return;
|
|
367
|
+
|
|
368
|
+
// Skip if already completed or exited
|
|
369
|
+
if (tracking.state === FlowState.COMPLETED || tracking.state === FlowState.EXITED) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Update flow state
|
|
374
|
+
tracking.state = FlowState.EXITED;
|
|
375
|
+
tracking.lastInteractionTime = Date.now();
|
|
376
|
+
|
|
377
|
+
// If we have a current step, update its data
|
|
378
|
+
if (tracking.currentStepId) {
|
|
379
|
+
const stepId = tracking.currentStepId;
|
|
380
|
+
ensureStepExists(tracking, stepId);
|
|
381
|
+
|
|
382
|
+
// Update time spent on current step
|
|
383
|
+
if (tracking.steps[stepId].lastVisitTime !== null) {
|
|
384
|
+
tracking.steps[stepId].timeSpent +=
|
|
385
|
+
Date.now() - tracking.steps[stepId].lastVisitTime;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Persist changes
|
|
390
|
+
persistFlowTracking(flowId);
|
|
391
|
+
|
|
392
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
393
|
+
console.debug(`[DAP] Legacy tracking call disabled - Flow ${flowId} exited:`, reason);
|
|
394
|
+
|
|
395
|
+
console.debug(`[DAP] Flow ${flowId} exited with reason: ${reason}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Track custom user actions within a flow (downloads, video plays, etc.)
|
|
400
|
+
* @param config The DAP configuration
|
|
401
|
+
* @param flowId The ID of the flow
|
|
402
|
+
* @param stepId The current step ID
|
|
403
|
+
* @param actionName Name of the action (e.g. download or video_play)
|
|
404
|
+
* @param actionDetails Additional details about the action
|
|
405
|
+
*/
|
|
406
|
+
export function trackCustomAction(
|
|
407
|
+
config: DapConfig,
|
|
408
|
+
flowId: string,
|
|
409
|
+
stepId: string,
|
|
410
|
+
actionName: string,
|
|
411
|
+
actionDetails: Record<string, any> = {}
|
|
412
|
+
): void {
|
|
413
|
+
const tracking = getFlowTracking(flowId);
|
|
414
|
+
if (!tracking) return;
|
|
415
|
+
|
|
416
|
+
// Legacy tracking disabled - using simple step tracking only
|
|
417
|
+
console.debug(`[DAP] Legacy tracking call disabled - Custom action "${actionName}" for flow ${flowId} at step ${stepId}`);
|
|
418
|
+
|
|
419
|
+
console.debug(`[DAP] Custom action "${actionName}" tracked for flow ${flowId} at step ${stepId}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Helper functions
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Ensure a step exists in the tracking object
|
|
426
|
+
*/
|
|
427
|
+
function ensureStepExists(tracking: FlowTrackingInfo, stepId: string): void {
|
|
428
|
+
if (!tracking.steps[stepId]) {
|
|
429
|
+
tracking.steps[stepId] = {
|
|
430
|
+
id: stepId,
|
|
431
|
+
state: StepState.NOT_VISITED,
|
|
432
|
+
firstVisitTime: null,
|
|
433
|
+
lastVisitTime: null,
|
|
434
|
+
timeSpent: 0,
|
|
435
|
+
interactions: 0
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get the element type from a selector
|
|
442
|
+
*/
|
|
443
|
+
function getElementType(selector?: string): string {
|
|
444
|
+
if (!selector) return 'unknown';
|
|
445
|
+
|
|
446
|
+
// Try to extract element type from selector
|
|
447
|
+
const elementMatch = selector.match(/^([a-zA-Z0-9-]+)/);
|
|
448
|
+
return elementMatch ? elementMatch[1].toLowerCase() : 'unknown';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Persist flow tracking to session storage
|
|
453
|
+
*/
|
|
454
|
+
function persistFlowTracking(flowId: string): void {
|
|
455
|
+
const tracking = flowRegistry.get(flowId);
|
|
456
|
+
if (!tracking) return;
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
sessionStorage.setItem(`dap_flow_tracking_${flowId}`, JSON.stringify(tracking));
|
|
460
|
+
} catch (e) {
|
|
461
|
+
console.error(`[DAP] Error persisting flow tracking for ${flowId}:`, e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Restore flow tracking from session storage
|
|
467
|
+
*/
|
|
468
|
+
function restoreFlowTracking(flowId: string): FlowTrackingInfo | null {
|
|
469
|
+
try {
|
|
470
|
+
const stored = sessionStorage.getItem(`dap_flow_tracking_${flowId}`);
|
|
471
|
+
if (!stored) return null;
|
|
472
|
+
|
|
473
|
+
const parsed = JSON.parse(stored) as FlowTrackingInfo;
|
|
474
|
+
|
|
475
|
+
// Convert steps back to proper structure (JSON stringification loses Map/Set)
|
|
476
|
+
const restoredTracking: FlowTrackingInfo = {
|
|
477
|
+
...parsed,
|
|
478
|
+
completedSteps: parsed.completedSteps || []
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
return restoredTracking;
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.error(`[DAP] Error restoring flow tracking for ${flowId}:`, e);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Calculate completion percentage
|
|
490
|
+
*/
|
|
491
|
+
function calculateCompletionPercentage(tracking: FlowTrackingInfo): number {
|
|
492
|
+
if (tracking.totalSteps === 0) return 0;
|
|
493
|
+
return Math.min(100, Math.round((tracking.completedSteps.length / tracking.totalSteps) * 100));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get total interactions across all steps
|
|
498
|
+
*/
|
|
499
|
+
function getTotalInteractions(tracking: FlowTrackingInfo): number {
|
|
500
|
+
let total = 0;
|
|
501
|
+
Object.values(tracking.steps).forEach(step => {
|
|
502
|
+
total += step.interactions;
|
|
503
|
+
});
|
|
504
|
+
return total;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Export the flow tracking system API
|
|
508
|
+
export const flowTrackingSystem = {
|
|
509
|
+
initializeFlowTracking,
|
|
510
|
+
getFlowTracking,
|
|
511
|
+
trackImpression,
|
|
512
|
+
startFlow,
|
|
513
|
+
trackStepTransition,
|
|
514
|
+
completeFlow,
|
|
515
|
+
exitFlow,
|
|
516
|
+
trackCustomAction
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Initialize tracking system
|
|
520
|
+
export function initializeFlowTrackingSystem(config: DapConfig, flowId: string) {
|
|
521
|
+
// Initialize tracking for the flow
|
|
522
|
+
initializeFlowTracking(flowId);
|
|
523
|
+
return flowTrackingSystem;
|
|
524
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/utils/idGenerator.ts
|
|
2
|
+
// Utility for consistent step ID generation and validation
|
|
3
|
+
|
|
4
|
+
// Registry for mapping DOM elements to their step IDs
|
|
5
|
+
const elementStepIdRegistry = new WeakMap<HTMLElement, string>();
|
|
6
|
+
|
|
7
|
+
// Store the current flow ID for context
|
|
8
|
+
let currentFlowContextId: string | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates if a step ID is in the correct format
|
|
12
|
+
* @param stepId The step ID to validate
|
|
13
|
+
* @returns Whether the step ID is valid
|
|
14
|
+
*/
|
|
15
|
+
export function isValidStepId(stepId: string | null | undefined): boolean {
|
|
16
|
+
if (!stepId) return false;
|
|
17
|
+
|
|
18
|
+
// Valid formats:
|
|
19
|
+
// 1. step-{number}
|
|
20
|
+
// 2. m{number}, t{number}, p{number}, s{number} (modal, tooltip, popover, survey)
|
|
21
|
+
// 3. Any alphanumeric string without special characters (except dash and underscore)
|
|
22
|
+
|
|
23
|
+
const validPattern = /^(step-\d+|[mtps]\d+|[a-zA-Z0-9_-]+)$/;
|
|
24
|
+
return validPattern.test(stepId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generates a valid step ID
|
|
29
|
+
* @param prefix The prefix for the step ID (step by default)
|
|
30
|
+
* @param index The index of the step (optional)
|
|
31
|
+
* @returns A valid step ID
|
|
32
|
+
*/
|
|
33
|
+
export function generateStepId(prefix: string = 'step', index?: number): string {
|
|
34
|
+
if (index !== undefined) {
|
|
35
|
+
return `${prefix}-${index}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate a deterministic step ID with timestamp and random suffix
|
|
39
|
+
const timestamp = Date.now().toString(36);
|
|
40
|
+
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
|
41
|
+
|
|
42
|
+
return `${prefix}-${timestamp}${randomSuffix}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalizes a step ID to ensure it follows the correct format
|
|
47
|
+
* @param stepId The step ID to normalize
|
|
48
|
+
* @param fallbackPrefix The prefix to use if a new ID needs to be generated
|
|
49
|
+
* @param index The index to use in the fallback ID
|
|
50
|
+
* @returns A normalized step ID
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeStepId(stepId: string | null | undefined, fallbackPrefix: string = 'step', index?: number): string {
|
|
53
|
+
if (isValidStepId(stepId)) {
|
|
54
|
+
return stepId as string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return generateStepId(fallbackPrefix, index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a step ID registry to ensure uniqueness
|
|
62
|
+
* @returns A registry object with methods to register and check step IDs
|
|
63
|
+
*/
|
|
64
|
+
export function createStepIdRegistry() {
|
|
65
|
+
const registry = new Set<string>();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
/**
|
|
69
|
+
* Register a step ID
|
|
70
|
+
* @param stepId The step ID to register
|
|
71
|
+
* @returns The registered step ID (may be modified if already exists)
|
|
72
|
+
*/
|
|
73
|
+
register(stepId: string): string {
|
|
74
|
+
let finalId = stepId;
|
|
75
|
+
let counter = 1;
|
|
76
|
+
|
|
77
|
+
// If the ID already exists, append a counter
|
|
78
|
+
while (registry.has(finalId)) {
|
|
79
|
+
finalId = `${stepId}-${counter}`;
|
|
80
|
+
counter++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
registry.add(finalId);
|
|
84
|
+
return finalId;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a step ID is registered
|
|
89
|
+
* @param stepId The step ID to check
|
|
90
|
+
* @returns Whether the step ID is registered
|
|
91
|
+
*/
|
|
92
|
+
has(stepId: string): boolean {
|
|
93
|
+
return registry.has(stepId);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all registered step IDs
|
|
98
|
+
* @returns Array of registered step IDs
|
|
99
|
+
*/
|
|
100
|
+
getAll(): string[] {
|
|
101
|
+
return Array.from(registry);
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear all registered step IDs
|
|
106
|
+
*/
|
|
107
|
+
clear(): void {
|
|
108
|
+
registry.clear();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create a global step ID registry for use across the SDK
|
|
114
|
+
export const globalStepIdRegistry = createStepIdRegistry();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the current flow context ID for step ID generation
|
|
118
|
+
* @param flowId The ID of the current flow
|
|
119
|
+
*/
|
|
120
|
+
export function setFlowContext(flowId: string | null): void {
|
|
121
|
+
currentFlowContextId = flowId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get the current flow context ID
|
|
126
|
+
* @returns The current flow context ID
|
|
127
|
+
*/
|
|
128
|
+
export function getFlowContext(): string | null {
|
|
129
|
+
return currentFlowContextId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Associate a step ID with a DOM element
|
|
134
|
+
* @param element The DOM element
|
|
135
|
+
* @param stepId The step ID
|
|
136
|
+
*/
|
|
137
|
+
export function registerElementStepId(element: HTMLElement, stepId: string): void {
|
|
138
|
+
if (!element || !stepId) return;
|
|
139
|
+
|
|
140
|
+
elementStepIdRegistry.set(element, stepId);
|
|
141
|
+
|
|
142
|
+
// Also set it as a data attribute for easier identification
|
|
143
|
+
if (!element.hasAttribute('data-dap-normalized-step')) {
|
|
144
|
+
element.setAttribute('data-dap-normalized-step', stepId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the step ID associated with a DOM element
|
|
150
|
+
* @param element The DOM element
|
|
151
|
+
* @returns The associated step ID, or null if not found
|
|
152
|
+
*/
|
|
153
|
+
export function getElementStepId(element: HTMLElement): string | null {
|
|
154
|
+
return elementStepIdRegistry.get(element) || null;
|
|
155
|
+
}
|