@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
|
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
|
package/src/tourUtils.ts
ADDED
|
File without changes
|
package/src/tracking.ts
ADDED
|
@@ -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
|
+
}
|