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