@sprig-technologies/sprig-browser 2.14.8 → 2.15.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.
Files changed (48) hide show
  1. package/index.d.ts +30 -7
  2. package/index.js +697 -181
  3. package/package.json +1 -1
  4. package/LICENSE +0 -22
  5. package/controller/controller.d.ts +0 -13
  6. package/controller/controller.d.ts.map +0 -1
  7. package/controller/controller.js +0 -1061
  8. package/controller/encodedViewJs.d.ts +0 -3
  9. package/controller/encodedViewJs.d.ts.map +0 -1
  10. package/controller/encodedViewJs.js +0 -1
  11. package/controller/iframe.d.ts +0 -9
  12. package/controller/iframe.d.ts.map +0 -1
  13. package/controller/iframe.js +0 -184
  14. package/controller/index.js +0 -3
  15. package/controller/queue.d.ts +0 -2
  16. package/controller/queue.d.ts.map +0 -1
  17. package/controller/queue.js +0 -95
  18. package/index.d.ts.map +0 -1
  19. package/shared/conflicting_widgets/index.d.ts +0 -5
  20. package/shared/conflicting_widgets/index.d.ts.map +0 -1
  21. package/shared/conflicting_widgets/index.js +0 -13
  22. package/shared/conflicting_widgets/intercom.d.ts +0 -3
  23. package/shared/conflicting_widgets/intercom.d.ts.map +0 -1
  24. package/shared/conflicting_widgets/intercom.js +0 -28
  25. package/shared/constants.d.ts +0 -49
  26. package/shared/constants.d.ts.map +0 -1
  27. package/shared/constants.js +0 -65
  28. package/shared/deferred.d.ts +0 -10
  29. package/shared/deferred.d.ts.map +0 -1
  30. package/shared/deferred.js +0 -15
  31. package/shared/eventEmitter.d.ts +0 -10
  32. package/shared/eventEmitter.d.ts.map +0 -1
  33. package/shared/eventEmitter.js +0 -52
  34. package/shared/network.d.ts +0 -8
  35. package/shared/network.d.ts.map +0 -1
  36. package/shared/network.js +0 -130
  37. package/shared/networkHelper.d.ts +0 -5
  38. package/shared/networkHelper.d.ts.map +0 -1
  39. package/shared/networkHelper.js +0 -9
  40. package/shared/shouldDirectEmbed.d.ts +0 -7
  41. package/shared/shouldDirectEmbed.d.ts.map +0 -1
  42. package/shared/shouldDirectEmbed.js +0 -8
  43. package/shared/tool.d.ts +0 -3
  44. package/shared/tool.d.ts.map +0 -1
  45. package/shared/tool.js +0 -19
  46. package/shared/ulEvents.d.ts +0 -33
  47. package/shared/ulEvents.d.ts.map +0 -1
  48. package/shared/ulEvents.js +0 -45
@@ -1,1061 +0,0 @@
1
- /*global UserLeap, window, document, Event, localStorage, Backbone, atob*/
2
- // Polyfills
3
- import 'core-js';
4
- import 'regenerator-runtime/runtime';
5
-
6
- import ConflictingWidgets from '../shared/conflicting_widgets';
7
- import { APP_MODES, CSS_CONSTANTS } from '../shared/constants';
8
- import { ulFetch, killNetworkRequests, getHttpHeaders } from '../shared/network';
9
- import { createFrame, createContainer, removeContainerOnClose, removeContainer } from '../controller/iframe';
10
- import { UserLeapQueue } from './queue';
11
- import eventEmitter from '../shared/eventEmitter';
12
- import { ulEvents, DISMISS_REASONS, SURVEY_STATE, internalEvents } from '../shared/ulEvents';
13
- import { calculateFrameHeight, overrideStyles } from '../shared/tool';
14
- import { v4 as uuidv4 } from 'uuid';
15
- import { delay } from '../shared/networkHelper';
16
- import shouldDirectEmbed from '../shared/shouldDirectEmbed';
17
- import {
18
- MOBILE_MAX_WIDTH,
19
- PAGE_URL_EVENT_NAME,
20
- MOBILE_PLATFORM_HEADERS,
21
- INSTALLATION_METHOD,
22
- } from '../shared/constants';
23
- import { encodedViewJs } from './encodedViewJs';
24
-
25
- const credentialsStorageKey = 'userleap.ids';
26
- const pageViewsStorageKey = 'userleap.pageviews';
27
- const dismissOnPageChangeEventTypes = ['popState', 'pushState', 'replaceState'];
28
-
29
- const PATH_ENV = 'environments';
30
- const PATH_VISITOR = 'visitors';
31
- const VIEW_TAG = 'ul-view-sdk-script';
32
- const MATCHERS_BY_TYPE = Object.freeze({
33
- contains: (pattern, url) => url.includes(pattern),
34
- notContains: (pattern, url) => !url.includes(pattern),
35
- exactly: (pattern, url) => url === pattern,
36
- notExactly: (pattern, url) => url !== pattern,
37
- startsWith: (pattern, url) => url.startsWith(pattern),
38
- endsWith: (pattern, url) => url.endsWith(pattern),
39
- regex: (pattern, url) => {
40
- return new RegExp(pattern).test(url);
41
- },
42
- legacy: (pattern, url) => {
43
- return new RegExp(pattern, 'i').test(url);
44
- },
45
- });
46
-
47
- function checkUrlMatch(event, url) {
48
- const { matchType, pattern } = event;
49
- const matcher = matchType ? MATCHERS_BY_TYPE[matchType] : MATCHERS_BY_TYPE.legacy;
50
-
51
- return matcher(pattern, url);
52
- }
53
-
54
- function shouldDisplaySurveyAfterDelay(triggeringEventId, trackStartUrl) {
55
- const { pageUrlEvents, interactiveEvents, dismissOnPageChange } = UserLeap._config;
56
- if (!dismissOnPageChange) return true;
57
-
58
- const urlAndInteractiveEvents = [];
59
- if (pageUrlEvents && pageUrlEvents.length) urlAndInteractiveEvents.push(...pageUrlEvents);
60
- if (interactiveEvents && interactiveEvents.length) urlAndInteractiveEvents.push(...interactiveEvents);
61
-
62
- const triggeringEvent = triggeringEventId && urlAndInteractiveEvents.find((event) => event.id === triggeringEventId);
63
- if (triggeringEvent) return checkUrlMatch(triggeringEvent, window.location.href);
64
- return trackStartUrl === window.location.href;
65
- }
66
-
67
- function locationChangeHandler(event) {
68
- const { pageUrlEvents, interactiveEvents, dismissOnPageChange, platform } = UserLeap._config;
69
- if (platform && platform !== 'web') return; // we should clean this up during web sdk controller refactor along with the should direct embed
70
- if (pageUrlEvents) trackPageView(window.location.href);
71
- if (interactiveEvents) {
72
- // need to remove old listeners bc current page may not be targeted
73
- removeInteractiveEventListener();
74
- addInteractiveEventListeners();
75
- }
76
- if (dismissOnPageChange && event && dismissOnPageChangeEventTypes.includes(event.type))
77
- UserLeap('dismissActiveSurvey', DISMISS_REASONS.PAGE_CHANGE);
78
- }
79
-
80
- const INTERACTIVE_LISTENER_OPTIONS = { capture: true };
81
-
82
- const addInteractiveEventListeners = () => {
83
- const activeEventsForUrl = UserLeap._config.interactiveEvents.filter((event) =>
84
- checkUrlMatch(event, window.location.href)
85
- );
86
-
87
- const handlersForActiveEvents = activeEventsForUrl.map((i) => {
88
- const { name, properties } = i;
89
- const { selector, innerText } = properties;
90
- return selector
91
- ? (e) => !!e.target.closest(selector) && UserLeap('track', name)
92
- : (e) => e.target.innerText === innerText && UserLeap('track', name);
93
- });
94
-
95
- const combinedHandler = (e) => handlersForActiveEvents.forEach((h) => h(e));
96
-
97
- UserLeap._config.interactiveEventsHandler = combinedHandler;
98
- window.addEventListener('click', combinedHandler, INTERACTIVE_LISTENER_OPTIONS);
99
- };
100
-
101
- const removeInteractiveEventListener = () => {
102
- window.removeEventListener('click', UserLeap._config.interactiveEventsHandler, INTERACTIVE_LISTENER_OPTIONS);
103
- delete UserLeap._config.interactiveEventsHandler;
104
- };
105
-
106
- function registerEventListeners() {
107
- ['hashchange', 'popstate'].forEach((evt) => window.addEventListener(evt, locationChangeHandler, true));
108
- }
109
-
110
- function unregisterEventListeners() {
111
- ['hashchange', 'popstate'].forEach((evt) => window.removeEventListener(evt, locationChangeHandler, true));
112
- if (UserLeap._config.interactiveEvents) removeInteractiveEventListener();
113
- }
114
-
115
- function apiUrl(version, fillPaths, trailing) {
116
- const components = [UserLeap._API_URL, 'sdk', version];
117
- if (fillPaths) {
118
- fillPaths.forEach((p) => {
119
- components.push(p);
120
- if (p === PATH_ENV) components.push(UserLeap.envId);
121
- else if (p === PATH_VISITOR) components.push(widgetGetVID());
122
- });
123
- }
124
- if (trailing) components.push(trailing);
125
- return components.join('/');
126
- }
127
-
128
- /**
129
- * Construct the get questions URL
130
- * @param {String} visitorId
131
- * @param {Object} config
132
- * @param {Object} options
133
- * @param {Number} options.surveyId Active SurveyId for the Environment display, from displaySurvey
134
- * @param {Number} options.surveyTemplateId SurveyTemplateId to display, from previewSurvey / Template Previews
135
- * @return {String}
136
- */
137
- function getQuestionURL(vid, options) {
138
- let url = apiUrl(1, [PATH_ENV], 'questions?');
139
- if (vid != null) url += `&vid=${vid}`;
140
- if (options) {
141
- if (options.surveyId) url += `&surveyid=${options.surveyId}`;
142
- if (options.surveyTemplateId) url += `&surveytemplateid=${options.surveyTemplateId}`;
143
- }
144
- return url;
145
- }
146
-
147
- function isLocalStorageAvailable() {
148
- try {
149
- if (typeof localStorage !== 'undefined') {
150
- localStorage.setItem('is_available', 'yes');
151
- if (localStorage.getItem('is_available') === 'yes') {
152
- localStorage.removeItem('is_available');
153
- return true;
154
- }
155
- }
156
- } catch (e) {
157
- return false;
158
- }
159
- return false;
160
- }
161
-
162
- function widgetGetLocalStorageCredentialsValue(field) {
163
- if (!UserLeap.localStorageAvailable) return null;
164
- const savedIdsJson = localStorage.getItem(credentialsStorageKey);
165
- if (savedIdsJson) {
166
- try {
167
- const savedIds = JSON.parse(savedIdsJson);
168
- const ids = savedIds[UserLeap.envId];
169
- return (ids && ids[field]) || null;
170
- } catch (err) {
171
- err.stack = savedIdsJson;
172
- UserLeap.reportError('Failed to parse local storage credentials', err);
173
- console.warn(`[Sprig] (ERR-427) Failed to lookup saved ids`, err);
174
- }
175
- }
176
- return null;
177
- }
178
-
179
- function widgetSetLocalStorageCredentialsValue(field, value) {
180
- if (!UserLeap.localStorageAvailable) return;
181
- const savedIdsJson = localStorage.getItem(credentialsStorageKey);
182
- let savedIds = {}; //default value
183
- if (savedIdsJson) {
184
- try {
185
- savedIds = JSON.parse(savedIdsJson);
186
- } catch (err) {
187
- err.stack = savedIdsJson;
188
- UserLeap.reportError('Failed to parse local storage credentials', err);
189
- console.warn(`[Sprig] (ERR-427) Failed to lookup saved ids`, err);
190
- }
191
- }
192
- let ids = savedIds[UserLeap.envId];
193
- if (ids) {
194
- ids[field] = value;
195
- } else {
196
- ids = { [field]: value };
197
- }
198
- savedIds[UserLeap.envId] = ids;
199
- try {
200
- localStorage.setItem(credentialsStorageKey, JSON.stringify(savedIds));
201
- } catch (err) {
202
- console.warn(`[Sprig] (ERR-426) Unable to write to Local Storage:: ${err.message}`);
203
- }
204
- }
205
-
206
- function generateVisitorId() {
207
- if (window.previewMode) return;
208
- UserLeap.visitorId = uuidv4();
209
- widgetSetLocalStorageCredentialsValue('vid', UserLeap.visitorId);
210
- eventEmitter.emit(ulEvents.VISITOR_ID_UPDATED, { visitorId: UserLeap.visitorId });
211
- }
212
-
213
- function widgetGetVID() {
214
- return window.previewMode ? 0 : UserLeap.visitorId;
215
- }
216
-
217
- function trackPageView(location) {
218
- if (!UserLeap.localStorageAvailable) return;
219
- if (location.endsWith('mock_snippet.html')) {
220
- return;
221
- }
222
- //filter out pages that aren't being tracked
223
- const pageUrlEvents = UserLeap._config.pageUrlEvents;
224
- if (pageUrlEvents && pageUrlEvents.length) {
225
- let shouldTrackPage = false;
226
- for (let i = 0; i < pageUrlEvents.length; i++) {
227
- shouldTrackPage = checkUrlMatch(pageUrlEvents[i], location);
228
-
229
- if (shouldTrackPage) {
230
- break;
231
- }
232
- }
233
- if (!shouldTrackPage) return;
234
- }
235
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig trackPageView', arguments);
236
- //Seconds that must elapse for the same location to be considered a new page view
237
- const sameLocationThreshold = 10;
238
- //Seconds that must elapse before considering a location change to be valid
239
- //this exists primarily to stop redirects from being counted several times
240
- const redirectThreshold = 1;
241
- let pageViews = [];
242
-
243
- const payload = {
244
- viewedAt: Date.now(),
245
- location: location,
246
- };
247
- const pageViewsStorage = localStorage.getItem(pageViewsStorageKey);
248
- try {
249
- if (pageViewsStorage) {
250
- //read existing views from local storage
251
- pageViews = JSON.parse(pageViewsStorage);
252
- if (pageViews.length > 0) {
253
- const latestPageView = pageViews[pageViews.length - 1];
254
- const secondsElapsed = (Date.now() - latestPageView.viewedAt) / 1000;
255
-
256
- //check location and time elapsed to dedupe and avoid overcounting on redirects
257
- if (
258
- (latestPageView.location != location && secondsElapsed > redirectThreshold) ||
259
- secondsElapsed > sameLocationThreshold
260
- ) {
261
- UserLeap._queue.push(['track', PAGE_URL_EVENT_NAME, { url: location }]);
262
- pageViews.push(payload);
263
- }
264
- }
265
- } else {
266
- UserLeap._queue.push(['track', PAGE_URL_EVENT_NAME, { url: location }]);
267
- pageViews.push(payload);
268
- }
269
-
270
- // Only keep the last 5 page views
271
- if (pageViews.length > 5) {
272
- pageViews.splice(0, pageViews.length - 5);
273
- }
274
-
275
- try {
276
- localStorage.setItem(pageViewsStorageKey, JSON.stringify(pageViews));
277
- } catch (err) {
278
- console.warn(`[Sprig] Unable to write to Local Storage: ${err.message}`);
279
- }
280
- } catch (err) {
281
- err.stack = pageViewsStorage;
282
- console.warn(`[Sprig] (ERR-425) Failed to update page views in local storage`);
283
- UserLeap.reportError('trackPageView', err);
284
- }
285
- }
286
-
287
- function bindTrackingEvents() {
288
- const historyRef = 'Backbone' in window && window.Backbone && Backbone.history ? Backbone.history : window.history;
289
-
290
- historyRef.pushState = ((f) =>
291
- function pushState() {
292
- const ret = f.apply(this, arguments);
293
- const event = new Event('pushState');
294
- window.dispatchEvent(event);
295
- locationChangeHandler(event);
296
- return ret;
297
- })(historyRef.pushState);
298
-
299
- historyRef.replaceState = ((f) =>
300
- function replaceState() {
301
- const ret = f.apply(this, arguments);
302
- const event = new Event('replaceState');
303
- window.dispatchEvent(event);
304
- locationChangeHandler(event);
305
- return ret;
306
- })(historyRef.replaceState);
307
- registerEventListeners();
308
- }
309
-
310
- async function getQuestions(options, submitAnswers) {
311
- const visitorId = widgetGetVID();
312
- if (options && !submitAnswers) {
313
- UserLeap._config.mode = APP_MODES.test;
314
- }
315
- const response = await authenticatedFetch(getQuestionURL(visitorId, options), {}, 0, true);
316
- if (!response.ok) {
317
- if (response.reportError) {
318
- console.warn(`[Sprig] (ERR-414) Failed to request questions from the server`, response.error);
319
- UserLeap.reportError('getQuestions', response.error);
320
- }
321
- return { success: false, surveyState: SURVEY_STATE.NO_SURVEY };
322
- }
323
- response.json.delay && (await delay(response.json.delay));
324
- return displayQuestions(response.json);
325
- }
326
-
327
- function displayQuestions(responseJson) {
328
- const { vid, uuid, questions, surveyId, context, locale, responseGroupUid, endCard, productConfig } = responseJson;
329
- const experimentFlags = responseJson.experimentFlags || {};
330
- const headers = getHttpHeaders(UserLeap);
331
- const isMobileWebview = calculateIsMobileWebview(headers);
332
- const useMobileStyling = calculateUseMobileStyling(headers);
333
- // Unrender if survey, vid or questions are undefined or empty
334
- if (vid == null || !questions || !questions.length) {
335
- return { success: false, message: '[Sprig] no survey found', surveyState: SURVEY_STATE.NO_SURVEY };
336
- }
337
- // Are we showing a Survey already?
338
- if (UserLeap.container) {
339
- const message = `[Sprig] (ERR-409) Found an existing Survey container, aborting rendering of this survey`;
340
- console.warn(message);
341
- return { success: false, message, surveyState: SURVEY_STATE.NO_SURVEY };
342
- }
343
- if (vid !== UserLeap.visitorId && uuid !== UserLeap.visitorId && !window.previewMode) {
344
- const message = 'Attempted to display survey to a different visitor';
345
- UserLeap.reportError('DisplaySurvey', new Error(message));
346
- return { success: false, message, surveyState: SURVEY_STATE.NO_SURVEY };
347
- }
348
-
349
- ConflictingWidgets.disable();
350
-
351
- let frameId, contentWinDocHead, contentWindow, hasOverlay;
352
- const verifyViewVersion = (data) => {
353
- const { [internalEvents.data.VIEW_VERSION]: viewSdkVersion } = data;
354
- if (viewSdkVersion !== headers['x-ul-sdk-version']) {
355
- removeContainer();
356
- }
357
- eventEmitter.removeListener(internalEvents.name.VERIFY_VIEW_VERSION, verifyViewVersion);
358
- };
359
- eventEmitter.subscribe(internalEvents.name.VERIFY_VIEW_VERSION, verifyViewVersion);
360
-
361
- if (shouldDirectEmbed(headers)) {
362
- frameId = 'ul-survey-frame';
363
- contentWinDocHead = document.head;
364
- contentWindow = window;
365
- hasOverlay = false;
366
- if (isMobileWebview) {
367
- createContainer();
368
- const mobileFrame = document.createElement('div');
369
- mobileFrame.id = frameId;
370
- UserLeap.container.appendChild(mobileFrame);
371
- removeContainerOnClose();
372
- eventEmitter.emit(ulEvents.SURVEY_LIFE_CYCLE, { state: 'presented' });
373
- eventEmitter.emit(ulEvents.SURVEY_PRESENTED, { name: ulEvents.SURVEY_PRESENTED });
374
- }
375
- } else {
376
- ({ frameId, contentWinDocHead, contentWindow, hasOverlay } = createFrame(productConfig, useMobileStyling));
377
- }
378
-
379
- UserLeap.frameId = frameId;
380
- UserLeap.useMobileStyling = useMobileStyling;
381
- const configureExitOnOverlayClick = (exit) => {
382
- eventEmitter.subscribe(ulEvents.CLOSE_SURVEY_ON_OVERLAY_CLICK, exit);
383
- };
384
- // Attach questions response to frame bootstrap config
385
- const config = Object.assign(
386
- {
387
- frame: frameId,
388
- envId: UserLeap.envId,
389
- surveyId,
390
- userId: uuid,
391
- cards: questions,
392
- context,
393
- locale,
394
- fontFamily: UserLeap.fontFamily,
395
- fontFamilyURL: UserLeap.fontFamilyURL,
396
- apiURL: UserLeap._API_URL,
397
- responseGroupUid,
398
- headers,
399
- endCard,
400
- useMobileStyling,
401
- mobilePlatform: UserLeap.mobilePlatform,
402
- mobileSDKVersion: UserLeap.mobileSDKVersion,
403
- experimentFlags,
404
- configureExitOnOverlayClick,
405
- eventEmitter,
406
- ulEvents,
407
- },
408
- UserLeap._config
409
- );
410
- if (UserLeap.customStyles) {
411
- config.customStyles = UserLeap.customStyles;
412
- }
413
- contentWindow.__cfg = config;
414
-
415
- function makeSafeScriptTag() {
416
- const scriptTag = document.createElement('script');
417
- if (UserLeap.nonce) {
418
- scriptTag.setAttribute('nonce', UserLeap.nonce);
419
- }
420
- scriptTag.id = VIEW_TAG;
421
- return scriptTag;
422
- }
423
- const frameSrc = UserLeap.viewSDKURL ? UserLeap.viewSDKURL : config.path;
424
-
425
- const existingScript = document.getElementById(VIEW_TAG);
426
- existingScript && existingScript.remove();
427
- // Inject frame application script
428
- const frameScript = makeSafeScriptTag();
429
-
430
- if (hasOverlay) {
431
- frameScript.addEventListener('load', () => {
432
- if (UserLeap.container) Object.assign(UserLeap.container.style, { display: 'flex' });
433
- });
434
- }
435
-
436
- if (config.installationMethod === INSTALLATION_METHOD.NPM) {
437
- frameScript.textContent = atob(encodedViewJs);
438
- } else {
439
- frameScript.src = frameSrc;
440
- contentWindow.addEventListener(
441
- 'error',
442
- (e) => {
443
- if (e.target.nodeName === 'SCRIPT' && e.target.src === frameSrc) {
444
- UserLeap.reportError('loadFrameScript', new Error(`Frame script failed to load`));
445
- }
446
- },
447
- {
448
- capture: true,
449
- once: true,
450
- }
451
- );
452
- }
453
- contentWinDocHead.appendChild(frameScript);
454
- return { success: true, surveyState: SURVEY_STATE.READY };
455
- }
456
-
457
- function calculateUseMobileStyling(headers) {
458
- if (UserLeap.useMobileStyling !== undefined) return UserLeap.useMobileStyling;
459
-
460
- // handle default-0 width iframes, if UserLeap.windowDimensions.width is not specified, with desktop styling
461
- const windowWidth = (UserLeap.windowDimensions && UserLeap.windowDimensions.width) || document.body.clientWidth;
462
- return calculateIsMobileWebview(headers) || (windowWidth > 10 && windowWidth < MOBILE_MAX_WIDTH);
463
- }
464
-
465
- function calculateIsMobileWebview(headers) {
466
- return MOBILE_PLATFORM_HEADERS.includes(headers['userleap-platform']);
467
- }
468
-
469
- function shuffleInteractiveEvents(events) {
470
- // All qualifying events will be sent when a user clicks the selected target element,
471
- // but we shuffle them here so that we don't bias towards always sending any particular
472
- // one first.
473
- let currentIndex = events.length;
474
- while (currentIndex) {
475
- const randomIndex = Math.floor(Math.random() * currentIndex);
476
- currentIndex -= 1;
477
- const tmp = events[currentIndex];
478
- events[currentIndex] = events[randomIndex];
479
- events[randomIndex] = tmp;
480
- }
481
- }
482
-
483
- /**
484
- * Gather identifier data, retrieve questions, and render widget
485
- * @param {Object} config [description]
486
- */
487
- function widgetInitialize(config) {
488
- if (!config) return;
489
- UserLeap._config = config;
490
-
491
- if (config.mute) {
492
- // Userleap.mute isn't defined yet
493
- UserLeap._queue.pause();
494
- }
495
-
496
- const { interactiveEvents, pageUrlEvents, dismissOnPageChange } = config;
497
- if (interactiveEvents) shuffleInteractiveEvents(interactiveEvents);
498
-
499
- if (interactiveEvents || pageUrlEvents || dismissOnPageChange) {
500
- bindTrackingEvents();
501
- locationChangeHandler();
502
- }
503
- }
504
-
505
- async function authenticatedFetch(url, options, attempt = 0, shouldDropOnRateLimit = false) {
506
- options.headers = Object.assign(getHttpHeaders(UserLeap), options.headers);
507
- const results = await ulFetch(url, options, attempt, shouldDropOnRateLimit);
508
- if (results.ok) {
509
- const authHeader = results.headers.get('Authorization');
510
- const tokenPieces = authHeader ? authHeader.split(' ') : undefined;
511
- const headerToken = tokenPieces && tokenPieces.length === 2 ? tokenPieces[1] : undefined;
512
- const visitorId = results.headers.get('x-ul-visitor-id');
513
- // reset the token and visitor id when they are valid and if either one of them is different from old values
514
- if (headerToken && visitorId && (visitorId !== UserLeap.visitorId || UserLeap.token !== headerToken)) {
515
- widgetSetLocalStorageCredentialsValue('token', headerToken);
516
- widgetSetLocalStorageCredentialsValue('vid', visitorId);
517
- eventEmitter.emit(ulEvents.VISITOR_ID_UPDATED, { visitorId });
518
- UserLeap.token = headerToken;
519
- UserLeap.visitorId = visitorId;
520
- }
521
- }
522
- return results;
523
- }
524
-
525
- /**
526
- * Hydrates the window.userLeap object with test mode
527
- * specific functions
528
- * @param {Object} config
529
- */
530
- const __enableUserLeapAPIActions = function(config) {
531
- if (!window.UserLeap) {
532
- return;
533
- }
534
-
535
- const identifyAndTrackHelper = async (payload = {}) => {
536
- const { userId, anonymousId, metadata = {} } = payload;
537
- let { eventName } = payload;
538
- if (UserLeap.debugMode && eventName !== PAGE_URL_EVENT_NAME) console.log('[DEBUG] Sprig track', arguments);
539
- if (config.mode === 'test') {
540
- return;
541
- }
542
- if (config.requireUserIdForTracking && !UserLeap.userId && !userId) {
543
- const message = '[Sprig] - Skipping tracking without userId';
544
- console.warn(message);
545
- return { success: false, message, surveyState: SURVEY_STATE.NO_SURVEY };
546
- }
547
- if (!eventName || eventName.trim().length === 0) {
548
- eventName = eventName ? String(eventName) : '';
549
- const message = `[Sprig] - Invalid event name ` + eventName;
550
- console.warn(message);
551
- return { success: false, message, surveyState: SURVEY_STATE.NO_SURVEY };
552
- }
553
- const trackStartUrl = window.location.href;
554
- if (!metadata.url) metadata.url = trackStartUrl;
555
-
556
- // set userId and/or anonymousId locally.
557
- // they will be included in request header.
558
- if (userId) UserLeap.userId = userId;
559
- if (anonymousId) UserLeap.partnerAnonymousId = anonymousId;
560
-
561
- const result = await authenticatedFetch(
562
- apiUrl(1, [PATH_VISITOR], 'events'),
563
- {
564
- body: JSON.stringify({
565
- event: eventName,
566
- metadata,
567
- }),
568
- method: 'POST',
569
- },
570
- 0,
571
- true
572
- );
573
-
574
- if (!result.ok) {
575
- const errorMessage = `[Sprig] (ERR-421) Failed to track event`;
576
- if (result.reportError) {
577
- console.warn(errorMessage, result.error);
578
- UserLeap.reportError('track', result.error);
579
- }
580
- // if setting user/anon id fails, queue is paused forever (unless client invokes UserLeap("unmute"))
581
- if (userId || anonymousId) killNetworkRequests('Disabled: Failed to set userId or anonymousId');
582
- return { success: false, message: errorMessage, error: result.error, surveyState: SURVEY_STATE.NO_SURVEY };
583
- }
584
- if (userId) widgetSetLocalStorageCredentialsValue('uid', userId);
585
- if (anonymousId) widgetSetLocalStorageCredentialsValue('aid', anonymousId);
586
-
587
- const responseBody = result.json;
588
- responseBody.delay && (await delay(responseBody.delay));
589
- if (shouldDisplaySurveyAfterDelay(responseBody.eventId, trackStartUrl)) {
590
- return displayQuestions(responseBody);
591
- } else {
592
- return {
593
- success: false,
594
- message: 'Study should not be displayed after page navigation',
595
- surveyState: SURVEY_STATE.NO_SURVEY,
596
- };
597
- }
598
- };
599
-
600
- const apiActions = {
601
- // Triggers displaying specified survey. Does submit answers!
602
- async displaySurvey(surveyId) {
603
- UserLeap('dismissActiveSurvey', DISMISS_REASONS.OVERRIDE);
604
- return getQuestions({ surveyId }, true);
605
- },
606
- // Triggers displaying specified survey template. Does not submit answers
607
- previewSurvey(surveyTemplateId) {
608
- UserLeap('dismissActiveSurvey', DISMISS_REASONS.OVERRIDE);
609
- getQuestions({ surveyTemplateId }, false);
610
- },
611
- // Triggers displaying specified survey. Does not submit answers
612
- reviewSurvey(surveyId) {
613
- UserLeap('dismissActiveSurvey', DISMISS_REASONS.OVERRIDE);
614
- getQuestions({ surveyId }, false);
615
- },
616
- mute() {
617
- UserLeap._queue.pause();
618
- },
619
- unmute() {
620
- UserLeap._queue.unpause();
621
- },
622
- setVisitorToken() {
623
- console.warn('[UserLeap] setVisitorToken is deprecated.');
624
- },
625
- dismissActiveSurvey(initiator = DISMISS_REASONS.API) {
626
- eventEmitter.emit(ulEvents.SURVEY_WILL_CLOSE, { initiator });
627
- },
628
- // Set an arbitrary attribute on the visitor
629
- async setAttribute(attribute, value) {
630
- if (!attribute || !value) return;
631
- return this.setAttributes({ [attribute]: value });
632
- },
633
- async setAttributes(attributes) {
634
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig setAttributes', arguments);
635
- if (config.mode === 'test') return;
636
- if (config.requireUserIdForTracking && !UserLeap.userId) {
637
- const message = '[Sprig] - Skipping tracking without userId';
638
- console.warn(message);
639
- return { success: false, message };
640
- }
641
- if (attributes === null || attributes === undefined || Object.keys(attributes).length === 0) {
642
- const message = `[Sprig] - Disregarding empty attributes provided`;
643
- console.warn(message);
644
- return { success: false, message };
645
- }
646
- const result = await authenticatedFetch(apiUrl(1, [PATH_ENV, PATH_VISITOR], 'attributes'), {
647
- body: JSON.stringify(attributes),
648
- method: 'PUT',
649
- });
650
- if (!result.ok && result.reportError) {
651
- console.warn(`[Sprig] (ERR-432) Set attributes failed`, result.error);
652
- UserLeap.reportError('setAttributes', result.error);
653
- }
654
- return { success: !!result.ok };
655
- },
656
- // identifies and sets attributes on visitor; see sprig-browser/index.js for documentation
657
- async identifyAndSetAttributes(payload) {
658
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig identifyAndSetAttributes', arguments);
659
- if (config.mode === 'test') return;
660
- if (
661
- payload === null ||
662
- typeof payload !== 'object' ||
663
- !(payload.userId || payload.anonymousId || payload.attributes)
664
- ) {
665
- const message = `[Sprig] - Disregarding empty payload provided`;
666
- console.warn(message);
667
- return { success: false, message };
668
- }
669
- const { userId, anonymousId, attributes } = payload;
670
-
671
- if (config.requireUserIdForTracking && !UserLeap.userId && !userId) {
672
- const message = '[Sprig] - Skipping tracking without userId';
673
- console.warn(message);
674
- return { success: false, message };
675
- }
676
-
677
- if (
678
- // no attributes to set, and the provided userId and/or anonymousId are already set locally; nothing to do.
679
- !attributes &&
680
- (!userId || UserLeap.userId === userId) &&
681
- (!anonymousId || UserLeap.partnerAnonymousId === anonymousId)
682
- ) {
683
- return { success: true };
684
- }
685
-
686
- const putUserBody = {};
687
- // set userId and/or anonymousId locally, and in potential request body if there are no attributes.
688
- // they will be included in request header and used in authentication.
689
- if (userId) putUserBody.userId = UserLeap.userId = userId;
690
- if (anonymousId) putUserBody.partnerAnonymousId = UserLeap.partnerAnonymousId = anonymousId;
691
-
692
- let result;
693
- if (attributes) {
694
- result = await authenticatedFetch(apiUrl(1, [PATH_ENV, PATH_VISITOR], 'attributes'), {
695
- body: JSON.stringify(attributes),
696
- method: 'PUT',
697
- });
698
- if (!result.ok && result.reportError) {
699
- console.warn(`[Sprig] (ERR-432) identifyAndSetAttributes failed`, result.error);
700
- UserLeap.reportError('identifyAndSetAttributes', result.error);
701
- }
702
- } else {
703
- result = await authenticatedFetch(apiUrl(1, [PATH_ENV, PATH_VISITOR]), {
704
- body: JSON.stringify(putUserBody),
705
- method: 'PUT',
706
- });
707
- }
708
-
709
- if (userId || anonymousId) {
710
- if (result.ok) {
711
- if (userId) widgetSetLocalStorageCredentialsValue('uid', userId);
712
- if (anonymousId) widgetSetLocalStorageCredentialsValue('aid', anonymousId);
713
- } else {
714
- // if setting user/anon id fails, queue is paused forever (unless client invokes UserLeap("unmute"))
715
- killNetworkRequests('Disabled: Failed to set userId or anonymousId');
716
- }
717
- }
718
-
719
- return { success: !!result.ok };
720
- },
721
- async removeAttributes(attributes) {
722
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig removeAttributes', arguments);
723
- if (config.mode === 'test') return;
724
- if (attributes === null || attributes === undefined || attributes.length === 0) {
725
- const message = `[Sprig] - Disregarding empty attributes provided`;
726
- console.warn(message);
727
- return { success: false, message };
728
- }
729
- if (config.requireUserIdForTracking && !UserLeap.userId) {
730
- const message = '[Sprig] - Skipping tracking without userId';
731
- console.warn(message);
732
- return { success: false, message };
733
- }
734
- const result = await authenticatedFetch(apiUrl(1, [PATH_ENV, PATH_VISITOR], 'attributes'), {
735
- body: JSON.stringify({ delete: attributes }),
736
- method: 'DELETE',
737
- });
738
- if (!result.ok && result.reportError) {
739
- console.warn(`[Sprig] (ERR-433) Remove attributes failed`, result.error);
740
- UserLeap.reportError('removeAttributes', result.error);
741
- }
742
- return { success: !!result.ok };
743
- },
744
- async addSurveyListener(listener) {
745
- eventEmitter.subscribe(ulEvents.SURVEY_LIFE_CYCLE, listener);
746
- },
747
- async removeSurveyListener(listener) {
748
- eventEmitter.removeListener(ulEvents.SURVEY_LIFE_CYCLE, listener);
749
- },
750
- async addListener(event, listener) {
751
- eventEmitter.subscribe(event, listener);
752
- },
753
- async removeListener(event, listener) {
754
- eventEmitter.removeListener(event, listener);
755
- },
756
- async removeAllListeners() {
757
- eventEmitter.removeAllListeners();
758
- },
759
- //Deprecated function name
760
- setVisitorAttribute(attribute, value) {
761
- console.warn('[Sprig] setVisitorAttribute is deprecated. Please use setAttribute');
762
- return apiActions.setAttribute(attribute, value);
763
- },
764
- // Attach an email address to visitor
765
- async setEmail(email) {
766
- return apiActions.setAttribute('!email', email);
767
- },
768
- // Deprecated function name
769
- async setVisitorEmail(email) {
770
- console.warn('[Sprig] setVisitorEmail is deprecated. Please use setEmail');
771
- return apiActions.setEmail(email);
772
- },
773
- /**
774
- * Attach a user id to the visitor
775
- */
776
- async setUserId(userId) {
777
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig setUserId', arguments);
778
- if (userId === null || userId === undefined) {
779
- const message = `[Sprig] - Invalid userId ${userId}`;
780
- console.warn(message);
781
- return { success: false, message };
782
- }
783
- if (config.mode === 'test' || userId === UserLeap.userId) {
784
- return;
785
- }
786
-
787
- UserLeap.userId = userId;
788
- const result = await authenticatedFetch(apiUrl(1, [PATH_ENV, PATH_VISITOR]), {
789
- body: JSON.stringify({
790
- userId,
791
- }),
792
- method: 'PUT',
793
- });
794
- if (!result.ok) {
795
- //if setUserId fails, queue is paused forever (unless client invokes UserLeap("unmute"))
796
- if (result.reportError) {
797
- console.warn(`[Sprig] (ERR-420) Failed to set user id`, result.error);
798
- UserLeap.reportError('setUserId', result.error);
799
- }
800
- killNetworkRequests('Disabled: Failed to set userId');
801
- return;
802
- }
803
- widgetSetLocalStorageCredentialsValue('uid', userId);
804
- },
805
-
806
- /**
807
- * Set a partner anonymous id for future requests -- to be deprecated, never documented.
808
- */
809
- async setPartnerAnonymousId(partnerAnonymousId) {
810
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig setPartnerAnonymousId', arguments);
811
- if (partnerAnonymousId === null || partnerAnonymousId === undefined) {
812
- const message = `[Sprig] - Invalid partnerAnonymousId ${partnerAnonymousId}`;
813
- console.warn(message);
814
- return { success: false, message };
815
- }
816
-
817
- UserLeap.partnerAnonymousId = partnerAnonymousId;
818
- return { success: true };
819
- },
820
-
821
- /**
822
- * track an event to show survey if eligible
823
- * @param {*} eventName
824
- * @param {*} metadata
825
- * @returns
826
- */
827
- async track(eventName, metadata = {}) {
828
- return await identifyAndTrackHelper({ eventName, metadata });
829
- },
830
- // identifies and tracks event on visitor; see sprig-browser/index.js for documentation
831
- async identifyAndTrack(payload) {
832
- return await identifyAndTrackHelper(payload);
833
- },
834
- applyStyles(styleString) {
835
- UserLeap.customStyles = styleString;
836
- if (UserLeap.container) {
837
- // survey already present override the styles
838
- // this will only work for web sdk as when sdk is direct embedded there would not be a frame
839
- const surveyDocument = UserLeap.container.children[0].contentDocument;
840
- const customStyle = surveyDocument.getElementById(CSS_CONSTANTS.CUSTOM_STYLE_TAG_ID);
841
- customStyle ? (customStyle.textContent = styleString) : overrideStyles(surveyDocument, styleString);
842
- }
843
- },
844
- /**
845
- set viewport dimensions, in int pixels. necessary if Sprig is installed in an iframe/component defaulting to 0 width and height.
846
- * @param {Number} width
847
- * @param {Number} height
848
- */
849
- setWindowDimensions(width, height) {
850
- const parsedWidth = parseInt(width, 10);
851
- const parsedHeight = parseInt(height, 10);
852
- UserLeap.windowDimensions = {
853
- width: !Number.isNaN(parsedWidth) && parsedWidth,
854
- height: !Number.isNaN(parsedHeight) && parsedHeight,
855
- };
856
- if (!UserLeap.frameId) return; // nothing to do if there's no survey frame
857
- const iframe = document.getElementById(UserLeap.frameId);
858
- if (!iframe) return;
859
- if (UserLeap.useMobileStyling) {
860
- // desktop style is not repsonsive, but still emit SURVEY_DIMENSIONS
861
- if (UserLeap.windowDimensions.width) iframe.style.width = `${UserLeap.windowDimensions.width}px`;
862
- if (UserLeap.windowDimensions.height) iframe.style.maxHeight = `${UserLeap.windowDimensions.height - 20}px`;
863
- iframe.style.height = calculateFrameHeight(iframe.contentDocument);
864
- }
865
- eventEmitter.emit(ulEvents.SURVEY_DIMENSIONS, {
866
- name: ulEvents.SURVEY_DIMENSIONS,
867
- contentFrameWidth: iframe.clientWidth,
868
- contentFrameHeight: iframe.clientHeight,
869
- });
870
- },
871
- teardown() {
872
- unregisterEventListeners();
873
- window.UserLeap('dismissActiveSurvey', DISMISS_REASONS.API);
874
- delete window.UserLeap;
875
- delete window.Sprig;
876
- delete window._Sprig;
877
- },
878
- };
879
-
880
- Object.assign(window.UserLeap, apiActions);
881
- };
882
-
883
- /**
884
- * Pulls down presentation config settings from server and
885
- * applies them to supplied config dict
886
- */
887
- async function applyRemoteConfig(config) {
888
- const headers = UserLeap.mobileHeadersJSON ? JSON.parse(UserLeap.mobileHeadersJSON) : {};
889
- let response = await ulFetch(apiUrl(1, [PATH_ENV], 'config'), { headers });
890
-
891
- const cspErrorName = 'TypeError';
892
- UserLeap.error = response.error;
893
- if (!response.ok && response.error && response.error.name === cspErrorName) {
894
- UserLeap._API_URL = 'https://api.userleap.com';
895
- UserLeap.reportError('sprigDomainRequest', response.error);
896
- response = await ulFetch(apiUrl(1, [PATH_ENV], 'config'), { headers });
897
- }
898
-
899
- if (!response.ok) {
900
- if (response.reportError) {
901
- console.warn(`[Sprig] (ERR-422) Failed to load configuration`, response.error);
902
- UserLeap.reportError('applyRemoteConfig', response.error);
903
- }
904
- killNetworkRequests('Disabled: failed to fetch configuration');
905
- return config;
906
- }
907
- const properties = response.json;
908
- if (properties && properties.disabled) {
909
- killNetworkRequests(`Disabled: ${properties.disabled}`);
910
- return;
911
- }
912
- const newConfig = Object.assign({}, config, properties);
913
- return newConfig;
914
- };
915
-
916
- async function reportError(action, err) {
917
- const mode = window.__cfg && window.__cfg.mode;
918
- const fetchFreshIfNotSet = false;
919
- const vid = widgetGetVID(fetchFreshIfNotSet);
920
- const envId = UserLeap.envId;
921
- const pde = window.document.documentElement;
922
- const meta = {
923
- mode,
924
- screenWidth: window.screen.width,
925
- screenHeight: window.screen.height,
926
- clientWidth: pde.clientWidth,
927
- clientHeight: pde.clientHeight,
928
- location: window.location.href,
929
- language: window.navigator.language,
930
- };
931
- const body = {
932
- action,
933
- err: { message: err.message, stack: err.stack },
934
- meta,
935
- vid,
936
- envId,
937
- };
938
- const result = await authenticatedFetch(
939
- apiUrl(1, null, 'errors'),
940
- {
941
- method: 'POST',
942
- headers: { 'x-ul-error': window.btoa(`userleap-${Date.now()}-error`) },
943
- body: JSON.stringify(body),
944
- },
945
- 0,
946
- true
947
- );
948
- if (!result.ok) {
949
- console.warn(`[Sprig] (ERR-444) Failed to report error to API`, err);
950
- }
951
- }
952
-
953
- function logoutUser() {
954
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig logout');
955
- //clear in-memory
956
- UserLeap.visitorId = null;
957
- UserLeap.userId = null;
958
- UserLeap.partnerAnonymousId = null;
959
- UserLeap.token = null;
960
- //clear local storage
961
- if (UserLeap.localStorageAvailable) {
962
- localStorage.removeItem(credentialsStorageKey);
963
- localStorage.removeItem(pageViewsStorageKey);
964
- }
965
- /**
966
- * If it's paused, we're not going to wait for it to unpause to replay
967
- * the queued items because it may replay it with a different visitorId
968
- */
969
- if (UserLeap._queue.isPaused()) UserLeap._queue.empty();
970
- generateVisitorId();
971
- UserLeap._queue.unpause();
972
- }
973
-
974
- /**
975
- * Initialize frame and bootstrap data
976
- * @param {Object} config this is included in the compilation step (points to the default viewSDKURL)
977
- * @param {String} config.envId Environment Id
978
- * @param {String} config.path Frame script url
979
- * @param {String} [config.border] Border color
980
- */
981
- export default function sprigInitializer(config = {}) {
982
- async function load() {
983
- if (UserLeap.loaded) return;
984
- UserLeap.UPDATES = ulEvents;
985
- UserLeap.reportError = reportError;
986
- UserLeap.logoutUser = logoutUser;
987
- UserLeap.loaded = true;
988
- //combine compile-provided config and installation-snippet config
989
- UserLeap._config = Object.assign({}, config, UserLeap.config);
990
- //backwards compat
991
- if (UserLeap._config && typeof UserLeap._config === 'object') {
992
- for (const attr in UserLeap._config) {
993
- UserLeap[attr] = UserLeap._config[attr];
994
- }
995
- }
996
- //backwards compatibility for old installation snippet which set appId instead of envId
997
- if (!UserLeap.envId) {
998
- if (UserLeap.appId) {
999
- UserLeap.envId = UserLeap.appId;
1000
- } else {
1001
- throw new Error('Missing Environment id');
1002
- }
1003
- }
1004
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig debug mode enabled');
1005
- const localStorageAvailable = isLocalStorageAvailable();
1006
- UserLeap.localStorageAvailable = localStorageAvailable;
1007
- const sampleRate = UserLeap.sampleRate;
1008
- if (sampleRate) {
1009
- if (!localStorageAvailable) {
1010
- if (UserLeap.debugMode) console.log('[DEBUG] Sprig cannot sample users without localStorage permissions');
1011
- } else {
1012
- let sampled = widgetGetLocalStorageCredentialsValue('sampled');
1013
- if (sampled === null) {
1014
- sampled = Math.random() < sampleRate;
1015
- widgetSetLocalStorageCredentialsValue('sampled', sampled);
1016
- }
1017
- if (!sampled) return;
1018
- }
1019
- } else if (localStorageAvailable) {
1020
- //if customer removed sampleRate config, need to remove from localStorage
1021
- const sampled = widgetGetLocalStorageCredentialsValue('sampled');
1022
- if (sampled !== null) {
1023
- widgetSetLocalStorageCredentialsValue('sampled', null);
1024
- }
1025
- }
1026
- if (!UserLeap._API_URL) UserLeap._API_URL = 'https://api.sprig.com';
1027
- //make a copy of existing queue, we will replay this after configuring
1028
- //Note: UserLeap._queue is an array at this point (defined in installation snippet)
1029
- const queueItems = [...UserLeap._queue];
1030
- //converts [] -> Queue()
1031
- UserLeap._queue = new UserLeapQueue(UserLeap, []);
1032
- UserLeap._queue.pause();
1033
- for (let i = 0; i < queueItems.length; i++) {
1034
- UserLeap._queue.push(queueItems[i]);
1035
- }
1036
-
1037
- const cachedToken = widgetGetLocalStorageCredentialsValue('token');
1038
- if (cachedToken) {
1039
- UserLeap.token = cachedToken;
1040
- UserLeap.visitorId = widgetGetLocalStorageCredentialsValue('vid');
1041
- UserLeap.userId = widgetGetLocalStorageCredentialsValue('uid');
1042
- UserLeap.partnerAnonymousId = widgetGetLocalStorageCredentialsValue('aid');
1043
- } else {
1044
- if (UserLeap.localStorageAvailable) localStorage.removeItem(credentialsStorageKey);
1045
- generateVisitorId();
1046
- }
1047
- const remoteConfig = await applyRemoteConfig(config);
1048
- __enableUserLeapAPIActions(remoteConfig);
1049
- await widgetInitialize(remoteConfig);
1050
- UserLeap._queue.unpause();
1051
- eventEmitter.emit(ulEvents.SDK_READY);
1052
- eventEmitter.emit(ulEvents.VISITOR_ID_UPDATED, { visitorId: UserLeap.visitorId });
1053
- }
1054
- if (document.readyState === 'complete') {
1055
- load();
1056
- } else if (window.attachEvent) {
1057
- window.attachEvent('onload', load);
1058
- } else {
1059
- window.addEventListener('load', load, false);
1060
- }
1061
- }