@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.
- package/LICENSE.md +176 -0
- package/README.md +23 -0
- package/controller/controller.js +1038 -0
- package/controller/encodedViewJs.js +1 -0
- package/controller/iframe.js +184 -0
- package/controller/index.js +3 -0
- package/controller/queue.js +96 -0
- package/controller/recordingAPI/index.js +166 -0
- package/controller/recordingAPI/permissionStubs.json +65 -0
- package/index.js +207 -0
- package/package.json +35 -0
- package/shared/conflicting_widgets/index.js +13 -0
- package/shared/conflicting_widgets/intercom.js +28 -0
- package/shared/constants.js +59 -0
- package/shared/deferred.js +15 -0
- package/shared/eventEmitter.js +52 -0
- package/shared/network.js +130 -0
- package/shared/networkHelper.js +9 -0
- package/shared/shouldDirectEmbed.js +8 -0
- package/shared/tool.js +19 -0
- package/shared/ulEvents.js +45 -0
|
@@ -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
|
+
}
|