@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.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. 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
+ }