@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
File without changes
@@ -0,0 +1,149 @@
1
+ export const tooltipCssText = `
2
+ :host {
3
+ --dap-z: 2147483638;
4
+ --dap-tip-bg: #1f2937;
5
+ --dap-tip-fg: #ffffff;
6
+ --dap-tip-border: rgba(255,255,255,0.12);
7
+ --dap-tip-radius: 10px;
8
+ --dap-tip-shadow: 0 10px 30px rgba(0,0,0,0.25), 0 2px 10px rgba(0,0,0,0.1);
9
+ --dap-tip-maxw: min(320px, 80vw);
10
+ --dap-highlight-color: rgba(59, 130, 246, 0.15);
11
+ --dap-highlight-outline: rgba(59, 130, 246, 0.4);
12
+ --dap-gap: 10px;
13
+ --dap-pad: 10px 12px;
14
+ color: var(--dap-tip-fg);
15
+ }
16
+
17
+ /* Base styles */
18
+ *, *::before, *::after {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ /* Tooltip layer */
23
+ .dap-tip-layer {
24
+ position: fixed;
25
+ inset: 0;
26
+ z-index: var(--dap-z);
27
+ pointer-events: none;
28
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
29
+ }
30
+
31
+ /* Ring highlight */
32
+ .dap-tip-ring {
33
+ position: fixed;
34
+ border-radius: 12px;
35
+ box-shadow: 0 0 0 2px var(--dap-highlight-outline) inset, 0 0 0 6px var(--dap-highlight-color);
36
+ transition: transform .12s ease, top .12s ease, left .12s ease, width .12s ease, height .12s ease;
37
+ animation: dapPulse 1.8s ease-in-out infinite;
38
+ pointer-events: none;
39
+ }
40
+
41
+ /* Tooltip bubble */
42
+ .dap-tip-bubble {
43
+ position: fixed;
44
+ max-width: var(--dap-tip-maxw);
45
+ background: var(--dap-tip-bg);
46
+ color: var(--dap-tip-fg);
47
+ border: 1px solid var(--dap-tip-border);
48
+ border-radius: var(--dap-tip-radius);
49
+ padding: var(--dap-pad);
50
+ box-shadow: var(--dap-tip-shadow);
51
+ font-size: 13.5px;
52
+ line-height: 1.45;
53
+ pointer-events: auto;
54
+ word-wrap: break-word;
55
+ z-index: calc(var(--dap-z) + 1);
56
+ }
57
+
58
+ .dap-tip-bubble a {
59
+ color: #93c5fd;
60
+ text-decoration: underline;
61
+ }
62
+
63
+ .dap-tip-bubble:focus-visible {
64
+ outline: 2px solid #2563eb;
65
+ outline-offset: 2px;
66
+ }
67
+
68
+ /* Arrow styles */
69
+ .dap-tip-bubble[data-placement^="top"]::after,
70
+ .dap-tip-bubble[data-placement^="bottom"]::after,
71
+ .dap-tip-bubble[data-placement^="left"]::after,
72
+ .dap-tip-bubble[data-placement^="right"]::after {
73
+ content: "";
74
+ position: absolute;
75
+ width: 10px;
76
+ height: 10px;
77
+ background: var(--dap-tip-bg);
78
+ border-left: 1px solid var(--dap-tip-border);
79
+ border-top: 1px solid var(--dap-tip-border);
80
+ transform: rotate(45deg);
81
+ }
82
+
83
+ .dap-tip-bubble[data-placement^="top"]::after {
84
+ bottom: -6px;
85
+ left: 16px;
86
+ border-left: 0;
87
+ border-top: 0;
88
+ border-right: 1px solid var(--dap-tip-border);
89
+ border-bottom: 1px solid var(--dap-tip-border);
90
+ }
91
+
92
+ .dap-tip-bubble[data-placement^="bottom"]::after {
93
+ top: -6px;
94
+ left: 16px;
95
+ }
96
+
97
+ .dap-tip-bubble[data-placement^="left"]::after {
98
+ right: -6px;
99
+ top: 12px;
100
+ border-left: 0;
101
+ border-top: 0;
102
+ border-right: 1px solid var(--dap-tip-border);
103
+ border-bottom: 1px solid var(--dap-tip-border);
104
+ }
105
+
106
+ .dap-tip-bubble[data-placement^="right"]::after {
107
+ left: -6px;
108
+ top: 12px;
109
+ }
110
+
111
+ /* Animations */
112
+ @keyframes dapPulse {
113
+ 0% {
114
+ box-shadow: 0 0 0 2px var(--dap-highlight-outline) inset, 0 0 0 4px var(--dap-highlight-color);
115
+ }
116
+ 50% {
117
+ box-shadow: 0 0 0 2px var(--dap-highlight-outline) inset, 0 0 0 8px rgba(37,99,235,0.30);
118
+ }
119
+ 100% {
120
+ box-shadow: 0 0 0 2px var(--dap-highlight-outline) inset, 0 0 0 4px var(--dap-highlight-color);
121
+ }
122
+ }
123
+
124
+ /* Responsive adjustments */
125
+ @media (max-width: 768px) {
126
+ .dap-tip-bubble {
127
+ max-width: calc(100vw - 20px);
128
+ font-size: 13px;
129
+ padding: 8px 10px;
130
+ }
131
+
132
+ .dap-tip-bubble[data-placement^="top"]::after,
133
+ .dap-tip-bubble[data-placement^="bottom"]::after {
134
+ left: 12px;
135
+ }
136
+
137
+ .dap-tip-bubble[data-placement^="left"]::after,
138
+ .dap-tip-bubble[data-placement^="right"]::after {
139
+ top: 8px;
140
+ }
141
+ }
142
+
143
+ /* Accessibility */
144
+ @media (prefers-reduced-motion: reduce) {
145
+ .dap-tip-ring {
146
+ animation: none;
147
+ }
148
+ }
149
+ `;
File without changes
File without changes
@@ -0,0 +1,223 @@
1
+ // src/tracking.ts
2
+ // Simple, deterministic step view tracking for DAP SDK
3
+
4
+ import type { DapConfig } from "./config";
5
+ import { userContextService } from './services/userContextService';
6
+
7
+ // =============================================================================
8
+ // SIMPLE TRACKING SYSTEM
9
+ // =============================================================================
10
+
11
+ /**
12
+ * In-memory tracking state to prevent duplicate tracking
13
+ */
14
+ class StepTrackingState {
15
+ private trackedSteps = new Set<string>();
16
+ private currentFlowId: string | null = null;
17
+
18
+ /**
19
+ * Check if a step has already been tracked for the current flow
20
+ */
21
+ isStepTracked(flowId: string, stepId: string): boolean {
22
+ if (this.currentFlowId !== flowId) {
23
+ // New flow started - reset tracking state
24
+ this.reset(flowId);
25
+ }
26
+
27
+ const key = `${flowId}:${stepId}`;
28
+ return this.trackedSteps.has(key);
29
+ }
30
+
31
+ /**
32
+ * Mark a step as tracked
33
+ */
34
+ markStepTracked(flowId: string, stepId: string): void {
35
+ if (this.currentFlowId !== flowId) {
36
+ this.reset(flowId);
37
+ }
38
+
39
+ const key = `${flowId}:${stepId}`;
40
+ this.trackedSteps.add(key);
41
+
42
+ console.debug(`[DAP Tracking] Step marked as tracked: ${key}`);
43
+ }
44
+
45
+ /**
46
+ * Reset tracking state for a new flow
47
+ */
48
+ reset(flowId: string): void {
49
+ this.currentFlowId = flowId;
50
+ this.trackedSteps.clear();
51
+ console.debug(`[DAP Tracking] Tracking state reset for flow: ${flowId}`);
52
+ }
53
+
54
+ /**
55
+ * Get current tracking state (for debugging)
56
+ */
57
+ getState(): { flowId: string | null; trackedCount: number; trackedSteps: string[] } {
58
+ return {
59
+ flowId: this.currentFlowId,
60
+ trackedCount: this.trackedSteps.size,
61
+ trackedSteps: Array.from(this.trackedSteps)
62
+ };
63
+ }
64
+ }
65
+
66
+ // Global tracking state
67
+ const trackingState = new StepTrackingState();
68
+
69
+ /**
70
+ * Track a step view - fire and forget
71
+ *
72
+ * @param flowId - The flow ID
73
+ * @param stepId - The step ID
74
+ * @param config - DAP configuration (optional, will use global if not provided)
75
+ */
76
+ export async function trackStepView(
77
+ flowId: string,
78
+ stepId: string,
79
+ config?: DapConfig
80
+ ): Promise<void> {
81
+ // Validation
82
+ if (!flowId || !stepId) {
83
+ console.warn('[DAP Tracking] Cannot track step: flowId and stepId are required');
84
+ return;
85
+ }
86
+
87
+ // Check for duplicate tracking
88
+ if (trackingState.isStepTracked(flowId, stepId)) {
89
+ console.debug(`[DAP Tracking] Step already tracked, skipping: ${flowId}:${stepId}`);
90
+ return;
91
+ }
92
+
93
+ // Get configuration
94
+ const dapConfig = config || (window as any).__DAP_CONFIG__;
95
+ if (!dapConfig) {
96
+ console.error('[DAP Tracking] No configuration available for tracking');
97
+ return;
98
+ }
99
+
100
+ // Get user ID
101
+ const userAnalyticsContext = userContextService.getAnalyticsContext();
102
+ const userId = userAnalyticsContext.userId;
103
+
104
+ if (!userId) {
105
+ console.warn('[DAP Tracking] No user ID available for tracking');
106
+ return;
107
+ }
108
+
109
+ // Mark step as tracked immediately to prevent duplicates
110
+ trackingState.markStepTracked(flowId, stepId);
111
+
112
+ // Prepare tracking payload (MVP - only required fields)
113
+ const payload = {
114
+ flowId,
115
+ stepId,
116
+ userId
117
+ };
118
+
119
+ // Build API URL
120
+ const apiUrl = buildTrackingApiUrl(dapConfig);
121
+ if (!apiUrl) {
122
+ console.error('[DAP Tracking] Could not build API URL');
123
+ return;
124
+ }
125
+
126
+ // Fire and forget - send tracking call
127
+ try {
128
+ console.debug('[DAP Tracking] Sending step view:', payload);
129
+
130
+ const response = await fetch(apiUrl, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'X-Host-Url': window.location.origin,
135
+ 'X-Api-Key': dapConfig.apikey || ''
136
+ },
137
+ body: JSON.stringify(payload)
138
+ });
139
+
140
+ if (!response.ok) {
141
+ console.warn(`[DAP Tracking] API call failed with status ${response.status}`);
142
+ } else {
143
+ console.debug(`[DAP Tracking] Step view tracked successfully: ${flowId}:${stepId}`);
144
+ }
145
+ } catch (error) {
146
+ console.warn('[DAP Tracking] Failed to send tracking call:', error);
147
+ // Note: We don't retry to avoid duplicates
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Build the tracking API URL
153
+ */
154
+ function buildTrackingApiUrl(config: DapConfig): string | null {
155
+ const { organizationid, siteid, apiurl } = config;
156
+
157
+ if (!organizationid || !siteid || !apiurl) {
158
+ console.error('[DAP Tracking] Missing required config fields for API URL:', {
159
+ hasOrganizationId: !!organizationid,
160
+ hasSiteId: !!siteid,
161
+ hasApiUrl: !!apiurl
162
+ });
163
+ return null;
164
+ }
165
+
166
+ const baseUrl = apiurl.replace(/\/$/, ''); // Remove trailing slash
167
+ return `${baseUrl}/analytics/organizationId/${organizationid}/siteCollectionId/${siteid}`;
168
+ }
169
+
170
+ /**
171
+ * Reset tracking state (useful when starting a new flow)
172
+ */
173
+ export function resetFlowTracking(flowId: string): void {
174
+ trackingState.reset(flowId);
175
+ }
176
+
177
+ /**
178
+ * Get current tracking state (for debugging)
179
+ */
180
+ export function getTrackingState(): { flowId: string | null; trackedCount: number; trackedSteps: string[] } {
181
+ return trackingState.getState();
182
+ }
183
+
184
+ // =============================================================================
185
+ // LEGACY COMPATIBILITY STUBS
186
+ // =============================================================================
187
+
188
+ /**
189
+ * @deprecated Use trackStepView() instead
190
+ */
191
+ export async function trackAction(): Promise<any> {
192
+ console.warn('[DAP Tracking] trackAction() is deprecated. Use trackStepView() instead.');
193
+ return Promise.resolve();
194
+ }
195
+
196
+ /**
197
+ * @deprecated No longer needed
198
+ */
199
+ export function setupActionTracking(): void {
200
+ console.warn('[DAP Tracking] setupActionTracking() is deprecated and no longer needed.');
201
+ }
202
+
203
+ // For backward compatibility - some files might import these
204
+ export const TrackingAction = {
205
+ FLOW_IMPRESSION: 'FLOW_IMPRESSION',
206
+ FLOW_STARTED: 'FLOW_STARTED',
207
+ FLOW_INTERACTION: 'FLOW_INTERACTION',
208
+ STEP_TRANSITION: 'STEP_TRANSITION',
209
+ FLOW_COMPLETED: 'FLOW_COMPLETED',
210
+ FLOW_EXITED: 'FLOW_EXITED',
211
+ STEP_COMPLETION: 'STEP_COMPLETION'
212
+ } as const;
213
+
214
+ export interface TrackActionPayload {
215
+ stepId?: string;
216
+ actionType?: string;
217
+ [key: string]: any;
218
+ }
219
+
220
+ export interface ClientInfo {
221
+ userId?: string;
222
+ [key: string]: any;
223
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Creates a debounced function that delays invoking the provided function
3
+ * until after 'wait' milliseconds have elapsed since the last time the debounced function was invoked.
4
+ *
5
+ * @param func The function to debounce
6
+ * @param wait The number of milliseconds to delay
7
+ * @returns A debounced version of the function
8
+ */
9
+ export function debounce<T extends (...args: any[]) => any>(
10
+ func: T,
11
+ wait: number
12
+ ): (...args: Parameters<T>) => void {
13
+ let timeout: ReturnType<typeof setTimeout> | null = null;
14
+
15
+ return function(this: any, ...args: Parameters<T>): void {
16
+ const context = this;
17
+
18
+ if (timeout !== null) {
19
+ clearTimeout(timeout);
20
+ }
21
+
22
+ timeout = setTimeout(() => {
23
+ func.apply(context, args);
24
+ timeout = null;
25
+ }, wait);
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Creates a throttled function that only invokes the provided function at most once per
31
+ * every 'limit' milliseconds.
32
+ *
33
+ * @param func The function to throttle
34
+ * @param limit The number of milliseconds to throttle invocations to
35
+ * @returns A throttled version of the function
36
+ */
37
+ export function throttle<T extends (...args: any[]) => any>(
38
+ func: T,
39
+ limit: number
40
+ ): (...args: Parameters<T>) => void {
41
+ let inThrottle = false;
42
+ let lastArgs: Parameters<T> | null = null;
43
+ let lastContext: any = null;
44
+
45
+ return function(this: any, ...args: Parameters<T>): void {
46
+ const context = this;
47
+
48
+ if (!inThrottle) {
49
+ func.apply(context, args);
50
+ inThrottle = true;
51
+
52
+ setTimeout(() => {
53
+ inThrottle = false;
54
+
55
+ if (lastArgs !== null) {
56
+ func.apply(lastContext, lastArgs);
57
+ lastArgs = null;
58
+ lastContext = null;
59
+ }
60
+ }, limit);
61
+ } else {
62
+ lastArgs = args;
63
+ lastContext = context;
64
+ }
65
+ };
66
+ }
@@ -0,0 +1,124 @@
1
+ // src/utils/eventSequenceValidator.ts
2
+ // Validates that tracking events occur in the proper sequence
3
+
4
+ import { TrackingAction } from './flowTrackingSystem';
5
+
6
+ // Define the allowed sequences of tracking events
7
+ const validSequences: Record<string, string[]> = {
8
+ // Valid starting events (no prerequisites)
9
+ initial: [
10
+ TrackingAction.FLOW_IMPRESSION,
11
+ ],
12
+
13
+ // Events that can follow FLOW_IMPRESSION
14
+ [TrackingAction.FLOW_IMPRESSION]: [
15
+ TrackingAction.FLOW_STARTED, // FLOW_STARTED is the consistent action type used instead of flow_initiation
16
+ TrackingAction.STEP_TRANSITION,
17
+ TrackingAction.FLOW_EXITED
18
+ ],
19
+
20
+ // Events that can follow FLOW_STARTED
21
+ [TrackingAction.FLOW_STARTED]: [
22
+ TrackingAction.FLOW_INTERACTION,
23
+ TrackingAction.STEP_TRANSITION,
24
+ TrackingAction.FLOW_EXITED
25
+ ],
26
+
27
+ // Events that can follow FLOW_INTERACTION
28
+ [TrackingAction.FLOW_INTERACTION]: [
29
+ TrackingAction.FLOW_INTERACTION,
30
+ TrackingAction.STEP_TRANSITION,
31
+ TrackingAction.FLOW_COMPLETED, // FLOW_COMPLETED is the consistent action type used instead of flow_completion
32
+ TrackingAction.FLOW_EXITED // FLOW_EXITED is the consistent action type used instead of flow_exit
33
+ ],
34
+
35
+ // Events that can follow STEP_TRANSITION
36
+ [TrackingAction.STEP_TRANSITION]: [
37
+ TrackingAction.FLOW_INTERACTION,
38
+ TrackingAction.STEP_TRANSITION,
39
+ TrackingAction.FLOW_COMPLETED,
40
+ TrackingAction.FLOW_EXITED
41
+ ],
42
+
43
+ // Events that can follow FLOW_COMPLETED
44
+ [TrackingAction.FLOW_COMPLETED]: [
45
+ // Terminal state - no valid next events
46
+ ],
47
+
48
+ // Events that can follow FLOW_EXITED
49
+ [TrackingAction.FLOW_EXITED]: [
50
+ // Terminal state - no valid next events
51
+ ],
52
+
53
+ // Special case for flow reentry
54
+ [TrackingAction.FLOW_REENTRY]: [
55
+ TrackingAction.FLOW_INTERACTION,
56
+ TrackingAction.STEP_TRANSITION,
57
+ TrackingAction.FLOW_COMPLETED,
58
+ TrackingAction.FLOW_EXITED
59
+ ],
60
+ };
61
+
62
+ // Keeps track of the last event for each flow
63
+ const flowLastEvents: Record<string, string> = {};
64
+
65
+ /**
66
+ * Validates that an event follows a valid sequence based on the previous event
67
+ * @param flowId The ID of the flow
68
+ * @param actionType The action type to validate
69
+ * @returns Whether the event is valid in the current sequence
70
+ */
71
+ export function validateEventSequence(flowId: string, actionType: string): {
72
+ valid: boolean;
73
+ reason?: string;
74
+ } {
75
+ // Get the last event for this flow
76
+ const lastEvent = flowLastEvents[flowId];
77
+
78
+ // If this is the first event for this flow, check if it's a valid initial event
79
+ if (!lastEvent) {
80
+ const isValidStart = validSequences.initial.includes(actionType as TrackingAction);
81
+ // Store this event as the last one
82
+ if (isValidStart) {
83
+ flowLastEvents[flowId] = actionType;
84
+ }
85
+
86
+ return {
87
+ valid: isValidStart,
88
+ reason: isValidStart ? undefined : `Invalid initial event: ${actionType}. Must be one of: ${validSequences.initial.join(', ')}`
89
+ };
90
+ }
91
+
92
+ // Look up valid next events based on the last event
93
+ const validNextEvents = validSequences[lastEvent as keyof typeof validSequences] || [];
94
+
95
+ // Check if the current event is a valid next event
96
+ const isValidNext = validNextEvents.includes(actionType as any);
97
+
98
+ // Store this event as the last one if it's valid
99
+ if (isValidNext) {
100
+ flowLastEvents[flowId] = actionType;
101
+ }
102
+
103
+ return {
104
+ valid: isValidNext,
105
+ reason: isValidNext ? undefined : `Invalid event sequence: ${lastEvent} -> ${actionType}. Valid next events: ${validNextEvents.join(', ')}`
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Reset the event sequence for a flow (useful for testing or manual resets)
111
+ * @param flowId The ID of the flow
112
+ */
113
+ export function resetEventSequence(flowId: string): void {
114
+ delete flowLastEvents[flowId];
115
+ }
116
+
117
+ /**
118
+ * Get the last event type for a flow
119
+ * @param flowId The ID of the flow
120
+ * @returns The last event type, or undefined if no events have been tracked
121
+ */
122
+ export function getLastEventType(flowId: string): string | undefined {
123
+ return flowLastEvents[flowId];
124
+ }