onboardme-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE-v2.md +225 -0
- package/dist/sdk.iife.js +348 -0
- package/package.json +22 -0
- package/src/__tests__/day1.test.ts +37 -0
- package/src/__tests__/day2.test.ts +447 -0
- package/src/__tests__/day3.test.ts +110 -0
- package/src/__tests__/day4.test.ts +115 -0
- package/src/__tests__/day5.test.ts +102 -0
- package/src/__tests__/snapshot-dom-collector.test.ts +153 -0
- package/src/__tests__/snapshot-sender.test.ts +111 -0
- package/src/__tests__/v2-integration.test.ts +305 -0
- package/src/__tests__/v2-positioner.test.ts +115 -0
- package/src/__tests__/v2-renderer.test.ts +189 -0
- package/src/__tests__/v2-types.test.ts +74 -0
- package/src/__tests__/week2-day1.test.ts +62 -0
- package/src/__tests__/week2-day2.test.ts +128 -0
- package/src/__tests__/week2-day3.test.ts +128 -0
- package/src/__tests__/week2-day4.test.ts +177 -0
- package/src/__tests__/week2-day5.test.ts +294 -0
- package/src/__tests__/week3-day1.test.ts +169 -0
- package/src/__tests__/week3-day2.test.ts +267 -0
- package/src/__tests__/week3-day3.test.ts +213 -0
- package/src/__tests__/week3-day4.test.ts +213 -0
- package/src/__tests__/week3-day5.test.ts +350 -0
- package/src/__tests__/week4-day1.test.ts +277 -0
- package/src/__tests__/week4-day2.test.ts +227 -0
- package/src/__tests__/week4-day3.test.ts +323 -0
- package/src/__tests__/week4-day4.test.ts +210 -0
- package/src/__tests__/week4-day5.test.ts +503 -0
- package/src/__tests__/week5-day1.test.ts +152 -0
- package/src/__tests__/week5-day2.test.ts +222 -0
- package/src/__tests__/week5-day3.test.ts +297 -0
- package/src/__tests__/week5-day4.test.ts +306 -0
- package/src/__tests__/week5-day5.test.ts +345 -0
- package/src/__tests__/week7-day5-api-flows.test.ts +353 -0
- package/src/auto-generate/context-collector.ts +47 -0
- package/src/auto-generate/flow-generator-client.ts +97 -0
- package/src/browser.ts +5 -0
- package/src/components/celebration.ts +44 -0
- package/src/components/checklist-css.ts +159 -0
- package/src/components/checklist.ts +295 -0
- package/src/components/modal-css.ts +96 -0
- package/src/components/modal.ts +171 -0
- package/src/components/shadow-host.ts +30 -0
- package/src/core/api-client.ts +39 -0
- package/src/core/api-flows.ts +204 -0
- package/src/core/config.ts +37 -0
- package/src/core/event-batcher.ts +169 -0
- package/src/core/sdk.ts +301 -0
- package/src/detection/user-detection.ts +55 -0
- package/src/index.ts +95 -0
- package/src/snapshot/dom-collector.ts +193 -0
- package/src/snapshot/sender.ts +105 -0
- package/src/storage/event-listener.ts +59 -0
- package/src/storage/progress-tracker.ts +78 -0
- package/src/styles/checklist-css.ts +159 -0
- package/src/styles/checklist.css +166 -0
- package/src/styles/modal-css.ts +96 -0
- package/src/styles/modal.css +102 -0
- package/src/utils/dom.ts +49 -0
- package/src/utils/fingerprint.ts +20 -0
- package/src/utils/logger.ts +17 -0
- package/src/v2/positioner.ts +105 -0
- package/src/v2/renderer.ts +287 -0
- package/src/v2/styles.ts +89 -0
- package/src/v2/types.ts +53 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +28 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { FlowConfig } from '@onboardme/types'
|
|
2
|
+
import { logger } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
export interface FlowsResponse {
|
|
5
|
+
flows: FlowConfig[]
|
|
6
|
+
checksumHash: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CachedFlows {
|
|
10
|
+
flows: FlowConfig[]
|
|
11
|
+
checksumHash: string
|
|
12
|
+
timestamp: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// In-memory cache for flows and their checksum
|
|
16
|
+
let _cachedFlows: CachedFlows | null = null
|
|
17
|
+
let _pollInterval: ReturnType<typeof setInterval> | null = null
|
|
18
|
+
let _lastChecksumHash: string = ''
|
|
19
|
+
|
|
20
|
+
const POLL_INTERVAL_MS = 60000 // 60 seconds
|
|
21
|
+
const CACHE_TIMEOUT_MS = 3600000 // 1 hour
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetches flows from the API endpoint with a 10s timeout.
|
|
25
|
+
* Returns null if fetch fails (network error, timeout, parse error).
|
|
26
|
+
* Never throws.
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchFlowsFromAPI(endpoint: string, apiKey: string): Promise<FlowsResponse | null> {
|
|
29
|
+
try {
|
|
30
|
+
const controller = new AbortController()
|
|
31
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
|
32
|
+
|
|
33
|
+
const response = await fetch(`${endpoint}/v1/flows`, {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: {
|
|
36
|
+
'x-api-key': apiKey,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
clearTimeout(timeoutId)
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
logger.warn(`Failed to fetch flows: HTTP ${response.status}`)
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = await response.json() as FlowsResponse
|
|
50
|
+
return data
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof Error) {
|
|
53
|
+
if (error.name === 'AbortError') {
|
|
54
|
+
logger.warn('Fetch flows timeout (10s)')
|
|
55
|
+
} else {
|
|
56
|
+
logger.warn(`Fetch flows error: ${error.message}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Loads flows from localStorage cache.
|
|
65
|
+
* Returns null if cache is missing or expired.
|
|
66
|
+
*/
|
|
67
|
+
export function loadFlowsFromCache(cacheKey: string): CachedFlows | null {
|
|
68
|
+
try {
|
|
69
|
+
const cached = localStorage.getItem(cacheKey)
|
|
70
|
+
if (!cached) return null
|
|
71
|
+
|
|
72
|
+
const data = JSON.parse(cached) as CachedFlows
|
|
73
|
+
const age = Date.now() - data.timestamp
|
|
74
|
+
if (age > CACHE_TIMEOUT_MS) {
|
|
75
|
+
logger.log('Flows cache expired (1h)')
|
|
76
|
+
localStorage.removeItem(cacheKey)
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return data
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.warn('Failed to parse flows cache')
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Saves flows to localStorage cache with timestamp.
|
|
89
|
+
*/
|
|
90
|
+
export function saveFlowsToCache(cacheKey: string, flows: FlowConfig[], checksumHash: string): void {
|
|
91
|
+
try {
|
|
92
|
+
const data: CachedFlows = {
|
|
93
|
+
flows,
|
|
94
|
+
checksumHash,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
}
|
|
97
|
+
localStorage.setItem(cacheKey, JSON.stringify(data))
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger.warn('Failed to save flows to cache')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Fetches flows from API or returns cached flows.
|
|
105
|
+
* On success, caches the flows and returns them.
|
|
106
|
+
* On API failure, returns cached flows if available.
|
|
107
|
+
* On all failure, returns empty array (silent fallback).
|
|
108
|
+
*/
|
|
109
|
+
export async function fetchFlowsWithFallback(
|
|
110
|
+
endpoint: string,
|
|
111
|
+
apiKey: string,
|
|
112
|
+
cacheKey: string,
|
|
113
|
+
): Promise<{ flows: FlowConfig[]; checksumHash: string }> {
|
|
114
|
+
// Try API first
|
|
115
|
+
const apiResponse = await fetchFlowsFromAPI(endpoint, apiKey)
|
|
116
|
+
if (apiResponse) {
|
|
117
|
+
saveFlowsToCache(cacheKey, apiResponse.flows, apiResponse.checksumHash)
|
|
118
|
+
_cachedFlows = {
|
|
119
|
+
flows: apiResponse.flows,
|
|
120
|
+
checksumHash: apiResponse.checksumHash,
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
}
|
|
123
|
+
return apiResponse
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fall back to cache
|
|
127
|
+
const cached = loadFlowsFromCache(cacheKey)
|
|
128
|
+
if (cached) {
|
|
129
|
+
logger.log('Using cached flows from localStorage')
|
|
130
|
+
_cachedFlows = cached
|
|
131
|
+
return {
|
|
132
|
+
flows: cached.flows,
|
|
133
|
+
checksumHash: cached.checksumHash,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// No cache and API failed — return empty
|
|
138
|
+
logger.warn('No flows available (API down, cache empty)')
|
|
139
|
+
return { flows: [], checksumHash: '' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Starts polling the API for flow updates every 60s.
|
|
144
|
+
* Only re-renders if checksumHash changes (intelligent caching).
|
|
145
|
+
* Polling stops and restarts on demand — see startFlowsPolling().
|
|
146
|
+
*/
|
|
147
|
+
export function startFlowsPolling(
|
|
148
|
+
endpoint: string,
|
|
149
|
+
apiKey: string,
|
|
150
|
+
cacheKey: string,
|
|
151
|
+
onFlowsUpdated: (flows: FlowConfig[], checksumHash: string) => void,
|
|
152
|
+
): void {
|
|
153
|
+
if (_pollInterval) return; // Already polling
|
|
154
|
+
|
|
155
|
+
logger.log('Starting flows polling (60s interval)')
|
|
156
|
+
|
|
157
|
+
// First check immediately
|
|
158
|
+
void (async () => {
|
|
159
|
+
const response = await fetchFlowsFromAPI(endpoint, apiKey)
|
|
160
|
+
if (response && response.checksumHash !== _lastChecksumHash) {
|
|
161
|
+
_lastChecksumHash = response.checksumHash
|
|
162
|
+
saveFlowsToCache(cacheKey, response.flows, response.checksumHash)
|
|
163
|
+
onFlowsUpdated(response.flows, response.checksumHash)
|
|
164
|
+
}
|
|
165
|
+
})()
|
|
166
|
+
|
|
167
|
+
// Then poll every 60s
|
|
168
|
+
_pollInterval = setInterval(async () => {
|
|
169
|
+
const response = await fetchFlowsFromAPI(endpoint, apiKey)
|
|
170
|
+
if (response && response.checksumHash !== _lastChecksumHash) {
|
|
171
|
+
logger.log('Flows updated (new checksum detected)')
|
|
172
|
+
_lastChecksumHash = response.checksumHash
|
|
173
|
+
saveFlowsToCache(cacheKey, response.flows, response.checksumHash)
|
|
174
|
+
onFlowsUpdated(response.flows, response.checksumHash)
|
|
175
|
+
}
|
|
176
|
+
}, POLL_INTERVAL_MS)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Stops the flows polling interval.
|
|
181
|
+
*/
|
|
182
|
+
export function stopFlowsPolling(): void {
|
|
183
|
+
if (_pollInterval) {
|
|
184
|
+
clearInterval(_pollInterval)
|
|
185
|
+
_pollInterval = null
|
|
186
|
+
logger.log('Flows polling stopped')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Gets the currently cached flows. Used by tests or debugging.
|
|
192
|
+
*/
|
|
193
|
+
export function getCachedFlows(): CachedFlows | null {
|
|
194
|
+
return _cachedFlows
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clears all cached flows and stops polling. Used in tests.
|
|
199
|
+
*/
|
|
200
|
+
export function _clearFlowsCache(): void {
|
|
201
|
+
_cachedFlows = null
|
|
202
|
+
_lastChecksumHash = ''
|
|
203
|
+
stopFlowsPolling()
|
|
204
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OnboardMeConfig } from '@onboardme/types';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates and normalises the raw config passed to OnboardMe.init().
|
|
6
|
+
* All failures are logger.warn — never throw.
|
|
7
|
+
* Returns a normalised config with defaults applied.
|
|
8
|
+
*/
|
|
9
|
+
export function validateConfig(raw: unknown): OnboardMeConfig | null {
|
|
10
|
+
if (!raw || typeof raw !== 'object') {
|
|
11
|
+
logger.warn('init() called with no config — OnboardMe will not run.');
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = raw as Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
if (!config['productId'] || typeof config['productId'] !== 'string') {
|
|
18
|
+
logger.warn('config.productId is required and must be a string — OnboardMe will not run.');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (config['flows'] !== undefined && !Array.isArray(config['flows'])) {
|
|
23
|
+
logger.warn('config.flows must be an array — defaulting to [].');
|
|
24
|
+
config['flows'] = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Apply defaults
|
|
28
|
+
return {
|
|
29
|
+
productId: config['productId'] as string,
|
|
30
|
+
eventsEndpoint: config['eventsEndpoint'] as string | undefined,
|
|
31
|
+
autoGenerate: config['autoGenerate'] as OnboardMeConfig['autoGenerate'] ?? undefined,
|
|
32
|
+
apiFlows: config['apiFlows'] as OnboardMeConfig['apiFlows'] ?? undefined,
|
|
33
|
+
flows: (config['flows'] as OnboardMeConfig['flows']) ?? [],
|
|
34
|
+
user: config['user'] as OnboardMeConfig['user'] ?? undefined,
|
|
35
|
+
debug: typeof config['debug'] === 'boolean' ? config['debug'] : false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { OnboardingEvent, EventType } from '@onboardme/types';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { postEvents } from './api-client.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Module state
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const MAX_QUEUE_SIZE = 100;
|
|
10
|
+
const AUTO_FLUSH_DELAY_MS = 5_000;
|
|
11
|
+
|
|
12
|
+
let _endpoint = '';
|
|
13
|
+
let _anonymousId = '';
|
|
14
|
+
let _userId: string | undefined;
|
|
15
|
+
let _queue: OnboardingEvent[] = [];
|
|
16
|
+
let _flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
+
let _listenersAttached = false;
|
|
18
|
+
let _visibilityHandler: (() => void) | null = null;
|
|
19
|
+
let _beforeUnloadHandler: (() => void) | null = null;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Public API
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configures the batcher with the endpoint and anonymousId to use for all events.
|
|
27
|
+
* Must be called once during init() before any pushEvent calls.
|
|
28
|
+
*/
|
|
29
|
+
export function configureBatcher(endpoint: string, anonymousId: string): void {
|
|
30
|
+
_endpoint = endpoint;
|
|
31
|
+
_anonymousId = anonymousId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Sets the authenticated userId to include on future events after identify(). */
|
|
35
|
+
export function setBatcherUserId(userId: string): void {
|
|
36
|
+
_userId = userId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Adds an event to the in-memory queue.
|
|
41
|
+
* Auto-fills: eventId, anonymousId, userId, pageUrl, timestamp.
|
|
42
|
+
* Starts the 5s auto-flush timer on the first event.
|
|
43
|
+
* Drops the oldest event if the queue exceeds MAX_QUEUE_SIZE.
|
|
44
|
+
*/
|
|
45
|
+
export function pushEvent(
|
|
46
|
+
eventType: EventType,
|
|
47
|
+
partial: Omit<OnboardingEvent, 'eventId' | 'anonymousId' | 'eventType' | 'pageUrl' | 'timestamp'> = {},
|
|
48
|
+
): void {
|
|
49
|
+
const event: OnboardingEvent = {
|
|
50
|
+
eventId: crypto.randomUUID(),
|
|
51
|
+
anonymousId: _anonymousId,
|
|
52
|
+
...((_userId !== undefined) ? { userId: _userId } : {}),
|
|
53
|
+
eventType,
|
|
54
|
+
pageUrl: typeof window !== 'undefined' ? window.location.href : '',
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
...partial,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (_queue.length >= MAX_QUEUE_SIZE) {
|
|
60
|
+
_queue.shift(); // drop oldest to prevent unbounded growth
|
|
61
|
+
logger.warn('event queue full — oldest event dropped');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_queue.push(event);
|
|
65
|
+
logger.log(`event queued: ${eventType} (queue size: ${_queue.length})`);
|
|
66
|
+
|
|
67
|
+
// Start auto-flush timer only on the first event
|
|
68
|
+
if (_queue.length === 1 && _flushTimer === null) {
|
|
69
|
+
_flushTimer = setTimeout(() => {
|
|
70
|
+
_flushTimer = null;
|
|
71
|
+
flushEvents().catch(() => {});
|
|
72
|
+
}, AUTO_FLUSH_DELAY_MS);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Flushes all queued events to the configured endpoint.
|
|
78
|
+
* Clears the queue and cancels the timer on success.
|
|
79
|
+
* On failure: logs a warn, leaves the queue intact for retry.
|
|
80
|
+
* Never throws.
|
|
81
|
+
*/
|
|
82
|
+
export async function flushEvents(): Promise<void> {
|
|
83
|
+
if (_queue.length === 0) return;
|
|
84
|
+
if (!_endpoint) {
|
|
85
|
+
logger.warn('flushEvents called before configureBatcher — events not sent');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cancel any pending auto-flush timer
|
|
90
|
+
if (_flushTimer !== null) {
|
|
91
|
+
clearTimeout(_flushTimer);
|
|
92
|
+
_flushTimer = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const batch = [..._queue];
|
|
96
|
+
const ok = await postEvents(_endpoint, batch);
|
|
97
|
+
|
|
98
|
+
if (ok) {
|
|
99
|
+
_queue = [];
|
|
100
|
+
logger.log(`flushed ${batch.length} event(s)`);
|
|
101
|
+
}
|
|
102
|
+
// On failure: queue retained so next flush can retry
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Attaches visibilitychange and beforeunload flush listeners.
|
|
107
|
+
* Safe to call multiple times — listeners are only registered once.
|
|
108
|
+
*
|
|
109
|
+
* - visibilitychange: flushes via fetch when the tab is hidden (user switches tabs).
|
|
110
|
+
* - beforeunload: flushes via sendBeacon — fire-and-forget POST that survives page unload.
|
|
111
|
+
* Regular fetch is unreliable during beforeunload; sendBeacon is the correct API for this.
|
|
112
|
+
*/
|
|
113
|
+
export function attachFlushListeners(): void {
|
|
114
|
+
if (_listenersAttached) return;
|
|
115
|
+
_listenersAttached = true;
|
|
116
|
+
|
|
117
|
+
_visibilityHandler = () => {
|
|
118
|
+
if (document.visibilityState === 'hidden') {
|
|
119
|
+
flushEvents().catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
_beforeUnloadHandler = () => {
|
|
124
|
+
if (_queue.length === 0 || !_endpoint) return;
|
|
125
|
+
const payload = JSON.stringify({ events: _queue });
|
|
126
|
+
const sent = navigator.sendBeacon(_endpoint, new Blob([payload], { type: 'application/json' }));
|
|
127
|
+
if (sent) {
|
|
128
|
+
_queue = [];
|
|
129
|
+
if (_flushTimer !== null) {
|
|
130
|
+
clearTimeout(_flushTimer);
|
|
131
|
+
_flushTimer = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
document.addEventListener('visibilitychange', _visibilityHandler);
|
|
137
|
+
window.addEventListener('beforeunload', _beforeUnloadHandler);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Removes flush listeners — only used in tests to prevent jsdom listener accumulation. */
|
|
141
|
+
export function _detachFlushListeners(): void {
|
|
142
|
+
if (_visibilityHandler) {
|
|
143
|
+
document.removeEventListener('visibilitychange', _visibilityHandler);
|
|
144
|
+
_visibilityHandler = null;
|
|
145
|
+
}
|
|
146
|
+
if (_beforeUnloadHandler) {
|
|
147
|
+
window.removeEventListener('beforeunload', _beforeUnloadHandler);
|
|
148
|
+
_beforeUnloadHandler = null;
|
|
149
|
+
}
|
|
150
|
+
_listenersAttached = false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Resets all batcher state — only used in tests. */
|
|
154
|
+
export function _resetBatcher(): void {
|
|
155
|
+
_detachFlushListeners();
|
|
156
|
+
if (_flushTimer !== null) {
|
|
157
|
+
clearTimeout(_flushTimer);
|
|
158
|
+
_flushTimer = null;
|
|
159
|
+
}
|
|
160
|
+
_endpoint = '';
|
|
161
|
+
_anonymousId = '';
|
|
162
|
+
_userId = undefined;
|
|
163
|
+
_queue = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Returns a copy of the current queue — only used in tests. */
|
|
167
|
+
export function _getQueue(): OnboardingEvent[] {
|
|
168
|
+
return [..._queue];
|
|
169
|
+
}
|