@ninetailed/experience.js 7.5.3 → 7.6.0-beta.2
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/index.cjs.d.ts +1 -0
- package/{index.cjs → index.cjs.js} +697 -375
- package/index.esm.js +1203 -0
- package/package.json +6 -9
- package/src/index.d.ts +1 -1
- package/src/lib/ElementSeenObserver.d.ts +1 -1
- package/src/lib/Ninetailed.d.ts +19 -5
- package/src/lib/NinetailedCorePlugin/NinetailedCorePlugin.d.ts +69 -0
- package/src/lib/{ninetailedCorePlugin → NinetailedCorePlugin}/index.d.ts +1 -1
- package/src/lib/constants.d.ts +1 -2
- package/src/lib/experience/makeExperienceSelectMiddleware.d.ts +9 -9
- package/src/lib/guards/hasComponentViewTrackingThreshold.d.ts +3 -0
- package/src/lib/guards/hasExperienceSelectionMiddleware.d.ts +1 -1
- package/src/lib/plugins/selectPluginsHavingExperienceSelectionMiddleware.d.ts +2 -2
- package/src/lib/plugins/selectPluginsHavingOnChangeEmitter.d.ts +1 -1
- package/src/lib/types/OnSelectVariant.d.ts +51 -0
- package/src/lib/types/index.d.ts +9 -9
- package/src/lib/types/interfaces/HasComponentViewTrackingThreshold.d.ts +3 -0
- package/src/lib/types/interfaces/HasExperienceSelectionMiddleware.d.ts +10 -10
- package/src/lib/types/interfaces/InterestedInHiddenPage.d.ts +1 -1
- package/src/lib/types/interfaces/InterestedInProfileChange.d.ts +1 -1
- package/src/lib/types/interfaces/InterestedInSeenElements.d.ts +1 -3
- package/src/lib/utils/EventBuilder.d.ts +221 -0
- package/src/lib/utils/noop.d.ts +1 -0
- package/index.js +0 -886
- package/src/lib/ninetailedCorePlugin/ninetailedCorePlugin.d.ts +0 -17
- package/src/lib/test-helpers/intersection-observer-test-helper.d.ts +0 -2
- package/src/lib/types/ElementSeenPayload.d.ts +0 -82
- package/src/lib/types/EventHandler.d.ts +0 -6
- package/src/lib/types/NinetailedPlugin.d.ts +0 -11
- package/src/lib/types/TrackingProperties.d.ts +0 -35
- /package/src/lib/{ninetailedCorePlugin → NinetailedCorePlugin}/Events/build-context.d.ts +0 -0
- /package/src/lib/{ninetailedCorePlugin → NinetailedCorePlugin}/Events/build-locale.d.ts +0 -0
- /package/src/lib/{ninetailedCorePlugin → NinetailedCorePlugin}/Events/index.d.ts +0 -0
- /package/src/lib/{ninetailedCorePlugin → NinetailedCorePlugin}/constants.d.ts +0 -0
package/index.esm.js
ADDED
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
import { FEATURES, buildComponentViewEvent, logger, ConsoleLogSink, buildPageEvent, buildTrackEvent, buildIdentifyEvent, unionBy, pipe, PageviewProperties, Properties, Traits, isPageViewEvent, isTrackEvent, isIdentifyEvent, isComponentViewEvent, selectHasVariants, selectExperience, selectVariant as selectVariant$1, selectBaselineWithVariants, NinetailedApiClient, OnLogLogSink, OnErrorLogSink } from '@ninetailed/experience.js-shared';
|
|
2
|
+
export { EXPERIENCE_TRAIT_PREFIX, isExperienceMatch, selectActiveExperiments, selectDistribution, selectExperience, selectBaselineWithVariants as selectExperienceBaselineWithVariants, selectVariant as selectExperienceVariant, selectVariants as selectExperienceVariants, selectHasVariants as selectHasExperienceVariants } from '@ninetailed/experience.js-shared';
|
|
3
|
+
import { NinetailedAnalyticsPlugin, HAS_SEEN_COMPONENT, HAS_SEEN_ELEMENT } from '@ninetailed/experience.js-plugin-analytics';
|
|
4
|
+
import Analytics from 'analytics';
|
|
5
|
+
import { v4 } from 'uuid';
|
|
6
|
+
|
|
7
|
+
const COMPONENT = 'component';
|
|
8
|
+
const COMPONENT_START = 'componentStart';
|
|
9
|
+
const PAGE_HIDDEN = 'page_hidden';
|
|
10
|
+
const HAS_SEEN_STICKY_COMPONENT = 'sticky_component_view';
|
|
11
|
+
|
|
12
|
+
const buildClientLocale = () => navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language;
|
|
13
|
+
|
|
14
|
+
const buildClientNinetailedRequestContext = () => ({
|
|
15
|
+
url: window.location.href,
|
|
16
|
+
referrer: document.referrer,
|
|
17
|
+
locale: buildClientLocale(),
|
|
18
|
+
userAgent: navigator.userAgent,
|
|
19
|
+
document: {
|
|
20
|
+
title: document.title
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Similar to _.throttle but waits for the promise to resolve.
|
|
26
|
+
* There is no "wait time" because you can simply await `Promise.timeout` inside `fn` to wait some time before the next call.
|
|
27
|
+
*/
|
|
28
|
+
function asyncThrottle(fn) {
|
|
29
|
+
let runningPromise;
|
|
30
|
+
let queuedPromise;
|
|
31
|
+
let nextArgs;
|
|
32
|
+
return async args => {
|
|
33
|
+
if (runningPromise) {
|
|
34
|
+
nextArgs = args;
|
|
35
|
+
if (queuedPromise) {
|
|
36
|
+
return queuedPromise;
|
|
37
|
+
} else {
|
|
38
|
+
queuedPromise = runningPromise.then(() => {
|
|
39
|
+
queuedPromise = undefined;
|
|
40
|
+
runningPromise = fn(nextArgs);
|
|
41
|
+
return runningPromise;
|
|
42
|
+
});
|
|
43
|
+
return queuedPromise;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
runningPromise = fn(args);
|
|
47
|
+
return runningPromise;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const LEGACY_ANONYMOUS_ID = '__anon_id';
|
|
53
|
+
const ANONYMOUS_ID = '__nt_anonymous_id__';
|
|
54
|
+
const DEBUG_FLAG = '__nt_debug__';
|
|
55
|
+
const PROFILE_FALLBACK_CACHE = '__nt_profile__';
|
|
56
|
+
const EXPERIENCES_FALLBACK_CACHE = '__nt_experiences__';
|
|
57
|
+
const PROFILE_CHANGE = 'profile-change';
|
|
58
|
+
const PROFILE_RESET = 'profile-reset';
|
|
59
|
+
const CONSENT = '__nt-consent__';
|
|
60
|
+
const SET_ENABLED_FEATURES = 'set-enabled-features';
|
|
61
|
+
const EMPTY_MERGE_ID = 'nt:empty-merge-id';
|
|
62
|
+
|
|
63
|
+
const PLUGIN_NAME = 'ninetailed:core';
|
|
64
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
65
|
+
class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
|
|
66
|
+
constructor({
|
|
67
|
+
apiClient,
|
|
68
|
+
locale,
|
|
69
|
+
ninetailed,
|
|
70
|
+
onInitProfileId,
|
|
71
|
+
buildClientContext
|
|
72
|
+
}) {
|
|
73
|
+
var _this;
|
|
74
|
+
super();
|
|
75
|
+
_this = this;
|
|
76
|
+
this.name = PLUGIN_NAME;
|
|
77
|
+
this._instance = void 0;
|
|
78
|
+
this.queue = [];
|
|
79
|
+
this.enabledFeatures = Object.values(FEATURES);
|
|
80
|
+
this.buildContext = buildClientNinetailedRequestContext;
|
|
81
|
+
this.onInitProfileId = void 0;
|
|
82
|
+
this.apiClient = void 0;
|
|
83
|
+
this.locale = void 0;
|
|
84
|
+
this.ninetailed = void 0;
|
|
85
|
+
this[HAS_SEEN_STICKY_COMPONENT] = async function ({
|
|
86
|
+
payload
|
|
87
|
+
}) {
|
|
88
|
+
const ctx = _this.buildContext();
|
|
89
|
+
return _this.enqueueEvent(buildComponentViewEvent({
|
|
90
|
+
messageId: payload.meta.rid,
|
|
91
|
+
timestamp: payload.meta.ts,
|
|
92
|
+
componentId: payload.componentId,
|
|
93
|
+
experienceId: payload.experienceId,
|
|
94
|
+
variantIndex: payload.variantIndex,
|
|
95
|
+
ctx
|
|
96
|
+
}));
|
|
97
|
+
};
|
|
98
|
+
this.methods = {
|
|
99
|
+
reset: async function (...args) {
|
|
100
|
+
logger.debug('Resetting profile.');
|
|
101
|
+
const instance = args[args.length - 1];
|
|
102
|
+
instance.dispatch({
|
|
103
|
+
type: PROFILE_RESET
|
|
104
|
+
});
|
|
105
|
+
instance.storage.removeItem(ANONYMOUS_ID);
|
|
106
|
+
instance.storage.removeItem(PROFILE_FALLBACK_CACHE);
|
|
107
|
+
instance.storage.removeItem(EXPERIENCES_FALLBACK_CACHE);
|
|
108
|
+
logger.debug('Removed old profile data from localstorage.');
|
|
109
|
+
if (typeof _this.onInitProfileId === 'function') {
|
|
110
|
+
const profileId = await _this.onInitProfileId(undefined);
|
|
111
|
+
if (typeof profileId === 'string') {
|
|
112
|
+
instance.storage.setItem(ANONYMOUS_ID, profileId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
await _this.ninetailed.track('nt_reset');
|
|
116
|
+
logger.info('Profile reset successful.');
|
|
117
|
+
await delay(10);
|
|
118
|
+
},
|
|
119
|
+
debug: async function (...args) {
|
|
120
|
+
const enabled = args[0];
|
|
121
|
+
const instance = args[args.length - 1];
|
|
122
|
+
const consoleLogSink = new ConsoleLogSink();
|
|
123
|
+
if (enabled) {
|
|
124
|
+
instance.storage.setItem(DEBUG_FLAG, true);
|
|
125
|
+
logger.addSink(consoleLogSink);
|
|
126
|
+
logger.info('Debug mode enabled.');
|
|
127
|
+
} else {
|
|
128
|
+
instance.storage.removeItem(DEBUG_FLAG);
|
|
129
|
+
logger.info('Debug mode disabled.');
|
|
130
|
+
logger.removeSink(consoleLogSink.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
this.flush = asyncThrottle(this._flush.bind(this));
|
|
135
|
+
if (onInitProfileId) {
|
|
136
|
+
this.onInitProfileId = onInitProfileId;
|
|
137
|
+
}
|
|
138
|
+
if (buildClientContext) {
|
|
139
|
+
this.buildContext = buildClientContext;
|
|
140
|
+
}
|
|
141
|
+
this.apiClient = apiClient;
|
|
142
|
+
this.ninetailed = ninetailed;
|
|
143
|
+
this.locale = locale;
|
|
144
|
+
}
|
|
145
|
+
async initialize({
|
|
146
|
+
instance
|
|
147
|
+
}) {
|
|
148
|
+
this._instance = instance;
|
|
149
|
+
if (instance.storage.getItem(DEBUG_FLAG)) {
|
|
150
|
+
logger.addSink(new ConsoleLogSink());
|
|
151
|
+
logger.info('Ninetailed Debug Mode is enabled.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// legacy support for the old anonymousId
|
|
155
|
+
const legacyAnonymousId = instance.storage.getItem(LEGACY_ANONYMOUS_ID);
|
|
156
|
+
if (legacyAnonymousId) {
|
|
157
|
+
logger.debug('Found legacy anonymousId, migrating to new one.', legacyAnonymousId);
|
|
158
|
+
instance.storage.setItem(ANONYMOUS_ID, legacyAnonymousId);
|
|
159
|
+
instance.storage.removeItem(LEGACY_ANONYMOUS_ID);
|
|
160
|
+
}
|
|
161
|
+
if (typeof this.onInitProfileId === 'function') {
|
|
162
|
+
const profileId = await this.onInitProfileId(instance.storage.getItem(ANONYMOUS_ID));
|
|
163
|
+
if (typeof profileId === 'string') {
|
|
164
|
+
instance.storage.setItem(ANONYMOUS_ID, profileId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
instance.on(SET_ENABLED_FEATURES, ({
|
|
168
|
+
payload
|
|
169
|
+
}) => {
|
|
170
|
+
this.enabledFeatures = payload.features || [];
|
|
171
|
+
});
|
|
172
|
+
logger.debug('Ninetailed Core plugin initialized.');
|
|
173
|
+
}
|
|
174
|
+
pageStart(params) {
|
|
175
|
+
return this.abortNonClientEvents(params);
|
|
176
|
+
}
|
|
177
|
+
async page({
|
|
178
|
+
payload
|
|
179
|
+
}) {
|
|
180
|
+
logger.info('Sending Page event.');
|
|
181
|
+
const ctx = this.buildContext();
|
|
182
|
+
return this.enqueueEvent(buildPageEvent({
|
|
183
|
+
messageId: payload.meta.rid,
|
|
184
|
+
timestamp: payload.meta.ts,
|
|
185
|
+
properties: payload.properties,
|
|
186
|
+
ctx
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
trackStart(params) {
|
|
190
|
+
return this.abortNonClientEvents(params);
|
|
191
|
+
}
|
|
192
|
+
async track({
|
|
193
|
+
payload
|
|
194
|
+
}) {
|
|
195
|
+
logger.info('Sending Track event.');
|
|
196
|
+
const ctx = this.buildContext();
|
|
197
|
+
return this.enqueueEvent(buildTrackEvent({
|
|
198
|
+
messageId: payload.meta.rid,
|
|
199
|
+
timestamp: payload.meta.ts,
|
|
200
|
+
event: payload.event,
|
|
201
|
+
properties: payload.properties,
|
|
202
|
+
ctx
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
identifyStart(params) {
|
|
206
|
+
return this.abortNonClientEvents(params);
|
|
207
|
+
}
|
|
208
|
+
async identify({
|
|
209
|
+
payload
|
|
210
|
+
}) {
|
|
211
|
+
logger.info('Sending Identify event.');
|
|
212
|
+
const ctx = this.buildContext();
|
|
213
|
+
if (payload.userId === EMPTY_MERGE_ID && (!payload.traits || typeof payload.traits === 'object' && Object.keys(payload.traits).length === 0)) {
|
|
214
|
+
logger.info('Skipping Identify event as no userId and no traits are set.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return this.enqueueEvent(buildIdentifyEvent({
|
|
218
|
+
messageId: payload.meta.rid,
|
|
219
|
+
timestamp: payload.meta.ts,
|
|
220
|
+
traits: payload.traits,
|
|
221
|
+
userId: payload.userId === EMPTY_MERGE_ID ? '' : payload.userId,
|
|
222
|
+
ctx
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
getComponentViewTrackingThreshold() {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
async onTrackExperience(properties) {
|
|
229
|
+
if (properties.experience.sticky) {
|
|
230
|
+
await this.instance.dispatch({
|
|
231
|
+
type: HAS_SEEN_STICKY_COMPONENT,
|
|
232
|
+
componentId: properties.selectedVariant.id,
|
|
233
|
+
experienceId: properties.experience.id,
|
|
234
|
+
variantIndex: properties.selectedVariantIndex
|
|
235
|
+
});
|
|
236
|
+
this.flush();
|
|
237
|
+
}
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
onTrackComponent() {
|
|
241
|
+
return Promise.resolve();
|
|
242
|
+
}
|
|
243
|
+
setItemStart({
|
|
244
|
+
abort,
|
|
245
|
+
payload
|
|
246
|
+
}) {
|
|
247
|
+
if (![ANONYMOUS_ID, DEBUG_FLAG, PROFILE_FALLBACK_CACHE, EXPERIENCES_FALLBACK_CACHE, CONSENT].includes(payload.key)) {
|
|
248
|
+
return abort();
|
|
249
|
+
}
|
|
250
|
+
return payload;
|
|
251
|
+
}
|
|
252
|
+
async enqueueEvent(event) {
|
|
253
|
+
this.queue = unionBy([event], this.queue, 'messageId');
|
|
254
|
+
}
|
|
255
|
+
abortNonClientEvents({
|
|
256
|
+
abort,
|
|
257
|
+
payload
|
|
258
|
+
}) {
|
|
259
|
+
if (typeof window !== 'object') {
|
|
260
|
+
return abort();
|
|
261
|
+
}
|
|
262
|
+
return payload;
|
|
263
|
+
}
|
|
264
|
+
get instance() {
|
|
265
|
+
if (!this._instance) {
|
|
266
|
+
throw new Error('Ninetailed Core plugin not initialized.');
|
|
267
|
+
}
|
|
268
|
+
return this._instance;
|
|
269
|
+
}
|
|
270
|
+
async _flush() {
|
|
271
|
+
const events = Object.assign([], this.queue);
|
|
272
|
+
logger.info('Start flushing events.');
|
|
273
|
+
this.queue = [];
|
|
274
|
+
if (!events.length) {
|
|
275
|
+
return {
|
|
276
|
+
success: true
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const anonymousId = this.instance.storage.getItem(ANONYMOUS_ID);
|
|
281
|
+
const {
|
|
282
|
+
profile,
|
|
283
|
+
experiences
|
|
284
|
+
} = await this.apiClient.upsertProfile({
|
|
285
|
+
profileId: anonymousId,
|
|
286
|
+
events
|
|
287
|
+
}, {
|
|
288
|
+
locale: this.locale,
|
|
289
|
+
enabledFeatures: this.enabledFeatures
|
|
290
|
+
});
|
|
291
|
+
this.instance.storage.setItem(ANONYMOUS_ID, profile.id);
|
|
292
|
+
this.instance.storage.setItem(PROFILE_FALLBACK_CACHE, profile);
|
|
293
|
+
this.instance.storage.setItem(EXPERIENCES_FALLBACK_CACHE, experiences);
|
|
294
|
+
logger.debug('Profile from api: ', profile);
|
|
295
|
+
logger.debug('Experiences from api: ', experiences);
|
|
296
|
+
this.instance.dispatch({
|
|
297
|
+
type: PROFILE_CHANGE,
|
|
298
|
+
profile,
|
|
299
|
+
experiences
|
|
300
|
+
});
|
|
301
|
+
await delay(20);
|
|
302
|
+
return {
|
|
303
|
+
success: true
|
|
304
|
+
};
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.debug('An error occurred during flushing the events: ', error);
|
|
307
|
+
const fallbackProfile = this.instance.storage.getItem(PROFILE_FALLBACK_CACHE);
|
|
308
|
+
const fallbackExperiences = this.instance.storage.getItem(EXPERIENCES_FALLBACK_CACHE) || [];
|
|
309
|
+
if (fallbackProfile) {
|
|
310
|
+
logger.debug('Found a fallback profile - will use this.');
|
|
311
|
+
this.instance.dispatch({
|
|
312
|
+
type: PROFILE_CHANGE,
|
|
313
|
+
profile: fallbackProfile,
|
|
314
|
+
experiences: fallbackExperiences
|
|
315
|
+
});
|
|
316
|
+
} else {
|
|
317
|
+
logger.debug('No fallback profile found - setting profile to null.');
|
|
318
|
+
this.instance.dispatch({
|
|
319
|
+
type: PROFILE_CHANGE,
|
|
320
|
+
profile: null,
|
|
321
|
+
experiences: fallbackExperiences,
|
|
322
|
+
error
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
success: false
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function _objectWithoutPropertiesLoose(source, excluded) {
|
|
333
|
+
if (source == null) return {};
|
|
334
|
+
var target = {};
|
|
335
|
+
var sourceKeys = Object.keys(source);
|
|
336
|
+
var key, i;
|
|
337
|
+
for (i = 0; i < sourceKeys.length; i++) {
|
|
338
|
+
key = sourceKeys[i];
|
|
339
|
+
if (excluded.indexOf(key) >= 0) continue;
|
|
340
|
+
target[key] = source[key];
|
|
341
|
+
}
|
|
342
|
+
return target;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
class ElementSeenObserver {
|
|
346
|
+
constructor(_options) {
|
|
347
|
+
this._intersectionObserver = void 0;
|
|
348
|
+
this._elementDelays = void 0;
|
|
349
|
+
this._intersectionTimers = void 0;
|
|
350
|
+
this._options = _options;
|
|
351
|
+
this._elementDelays = new WeakMap();
|
|
352
|
+
this._intersectionTimers = new WeakMap();
|
|
353
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
354
|
+
this._intersectionObserver = new IntersectionObserver(this.onIntersection.bind(this));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
onIntersection(entries) {
|
|
358
|
+
entries.forEach(entry => {
|
|
359
|
+
const {
|
|
360
|
+
isIntersecting,
|
|
361
|
+
target
|
|
362
|
+
} = entry;
|
|
363
|
+
if (isIntersecting) {
|
|
364
|
+
const delays = this._elementDelays.get(target);
|
|
365
|
+
delays == null || delays.forEach(delay => {
|
|
366
|
+
const timeOut = window.setTimeout(() => {
|
|
367
|
+
this._options.onElementSeen(target, delay);
|
|
368
|
+
}, delay);
|
|
369
|
+
const currentTimers = this._intersectionTimers.get(target) || [];
|
|
370
|
+
this._intersectionTimers.set(target, [...currentTimers, timeOut]);
|
|
371
|
+
});
|
|
372
|
+
} else {
|
|
373
|
+
const timeOuts = this._intersectionTimers.get(target);
|
|
374
|
+
timeOuts == null || timeOuts.forEach(timeOut => {
|
|
375
|
+
if (typeof timeOut !== 'undefined') {
|
|
376
|
+
window.clearTimeout(timeOut);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
observe(element, options) {
|
|
383
|
+
var _this$_intersectionOb;
|
|
384
|
+
const delays = this._elementDelays.get(element) || [];
|
|
385
|
+
this._elementDelays.set(element, Array.from(new Set([...delays, (options == null ? void 0 : options.delay) || 0])));
|
|
386
|
+
(_this$_intersectionOb = this._intersectionObserver) == null || _this$_intersectionOb.observe(element);
|
|
387
|
+
}
|
|
388
|
+
unobserve(element) {
|
|
389
|
+
var _this$_intersectionOb2;
|
|
390
|
+
(_this$_intersectionOb2 = this._intersectionObserver) == null || _this$_intersectionOb2.unobserve(element);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const acceptsCredentials = plugin => {
|
|
395
|
+
return typeof plugin === 'object' && plugin !== null && 'setCredentials' in plugin && typeof plugin.setCredentials === 'function';
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const isInterestedInHiddenPage = arg => {
|
|
399
|
+
return typeof arg === 'object' && arg !== null && PAGE_HIDDEN in arg && typeof arg[PAGE_HIDDEN] === 'function';
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const decodeExperienceVariantsMap = encodedExperienceVariantsMap => {
|
|
403
|
+
return encodedExperienceVariantsMap.split(',').map(experienceIdWithVariant => {
|
|
404
|
+
const [experienceId, _variantIndex] = experienceIdWithVariant.split('=');
|
|
405
|
+
const variantIndex = parseInt(_variantIndex);
|
|
406
|
+
if (!experienceId || !variantIndex) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
experienceId,
|
|
411
|
+
variantIndex
|
|
412
|
+
};
|
|
413
|
+
}).filter(x => !!x).reduce((acc, curr) => Object.assign({}, acc, {
|
|
414
|
+
[curr.experienceId]: curr.variantIndex
|
|
415
|
+
}), {});
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
class OnChangeEmitter {
|
|
419
|
+
constructor() {
|
|
420
|
+
this.onChangeListeners = [];
|
|
421
|
+
}
|
|
422
|
+
addListener(listener) {
|
|
423
|
+
this.onChangeListeners.push(listener);
|
|
424
|
+
return () => {
|
|
425
|
+
this.removeOnChangeListener(listener);
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
invokeListeners() {
|
|
429
|
+
this.onChangeListeners.forEach(listener => listener());
|
|
430
|
+
}
|
|
431
|
+
removeOnChangeListener(listener) {
|
|
432
|
+
this.onChangeListeners = this.onChangeListeners.filter(l => l !== listener);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const hasOnChangeEmitter = arg => {
|
|
437
|
+
return typeof arg === 'object' && arg !== null && 'onChangeEmitter' in arg && typeof arg.onChangeEmitter === 'object' && arg.onChangeEmitter !== null && arg.onChangeEmitter.constructor === OnChangeEmitter;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const selectPluginsHavingOnChangeEmitter = plugins => {
|
|
441
|
+
const filteredPlugins = [];
|
|
442
|
+
for (const plugin of plugins) {
|
|
443
|
+
if (hasOnChangeEmitter(plugin)) {
|
|
444
|
+
filteredPlugins.push(plugin);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return filteredPlugins;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const hasExperienceSelectionMiddleware = arg => {
|
|
451
|
+
return typeof arg === 'object' && arg !== null && 'getExperienceSelectionMiddleware' in arg && typeof arg.getExperienceSelectionMiddleware === 'function';
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const selectPluginsHavingExperienceSelectionMiddleware = plugins => {
|
|
455
|
+
const filteredPlugins = [];
|
|
456
|
+
for (const plugin of plugins) {
|
|
457
|
+
if (hasExperienceSelectionMiddleware(plugin)) {
|
|
458
|
+
filteredPlugins.push(plugin);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return filteredPlugins;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const createPassThroughMiddleware = () => {
|
|
465
|
+
return ({
|
|
466
|
+
experience,
|
|
467
|
+
variant,
|
|
468
|
+
variantIndex
|
|
469
|
+
}) => {
|
|
470
|
+
return {
|
|
471
|
+
experience,
|
|
472
|
+
variant,
|
|
473
|
+
variantIndex
|
|
474
|
+
};
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
const makeExperienceSelectMiddleware = ({
|
|
478
|
+
plugins,
|
|
479
|
+
onChange,
|
|
480
|
+
experiences,
|
|
481
|
+
baseline,
|
|
482
|
+
profile
|
|
483
|
+
}) => {
|
|
484
|
+
let removeChangeListeners = [];
|
|
485
|
+
const pluginsHavingChangeEmitters = selectPluginsHavingOnChangeEmitter(plugins);
|
|
486
|
+
const prepareMiddleware = () => {
|
|
487
|
+
if (profile === null) {
|
|
488
|
+
return createPassThroughMiddleware();
|
|
489
|
+
}
|
|
490
|
+
const pluginsWithMiddleware = selectPluginsHavingExperienceSelectionMiddleware(plugins);
|
|
491
|
+
const middlewareFunctions = pluginsWithMiddleware.map(plugin => plugin.getExperienceSelectionMiddleware({
|
|
492
|
+
experiences,
|
|
493
|
+
baseline
|
|
494
|
+
}));
|
|
495
|
+
return pipe(...middlewareFunctions);
|
|
496
|
+
};
|
|
497
|
+
const middleware = prepareMiddleware();
|
|
498
|
+
const addListeners = () => {
|
|
499
|
+
removeChangeListeners = pluginsHavingChangeEmitters.map(plugin => {
|
|
500
|
+
const listener = () => {
|
|
501
|
+
onChange(middleware);
|
|
502
|
+
};
|
|
503
|
+
return plugin.onChangeEmitter.addListener(listener);
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
const removeListeners = () => {
|
|
507
|
+
removeChangeListeners.forEach(listener => listener());
|
|
508
|
+
};
|
|
509
|
+
return {
|
|
510
|
+
addListeners,
|
|
511
|
+
removeListeners,
|
|
512
|
+
middleware
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
class EventBuilder {
|
|
517
|
+
constructor(buildRequestContext) {
|
|
518
|
+
this.buildRequestContext = void 0;
|
|
519
|
+
this.buildRequestContext = buildRequestContext || buildClientNinetailedRequestContext;
|
|
520
|
+
}
|
|
521
|
+
page(properties, data) {
|
|
522
|
+
return buildPageEvent(Object.assign({
|
|
523
|
+
messageId: (data == null ? void 0 : data.messageId) || v4()
|
|
524
|
+
}, data, {
|
|
525
|
+
timestamp: Date.now(),
|
|
526
|
+
properties: properties || {},
|
|
527
|
+
ctx: this.buildRequestContext()
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
track(event, properties, data) {
|
|
531
|
+
return buildTrackEvent(Object.assign({
|
|
532
|
+
messageId: (data == null ? void 0 : data.messageId) || v4(),
|
|
533
|
+
timestamp: Date.now()
|
|
534
|
+
}, data, {
|
|
535
|
+
event,
|
|
536
|
+
properties: properties || {},
|
|
537
|
+
ctx: this.buildRequestContext()
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
identify(userId, traits, data) {
|
|
541
|
+
return buildIdentifyEvent(Object.assign({
|
|
542
|
+
messageId: (data == null ? void 0 : data.messageId) || v4(),
|
|
543
|
+
timestamp: Date.now()
|
|
544
|
+
}, data, {
|
|
545
|
+
traits: traits || {},
|
|
546
|
+
userId: userId || '',
|
|
547
|
+
ctx: this.buildRequestContext()
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
component(componentId, experienceId, variantIndex, data) {
|
|
551
|
+
return buildComponentViewEvent(Object.assign({
|
|
552
|
+
messageId: (data == null ? void 0 : data.messageId) || v4(),
|
|
553
|
+
timestamp: Date.now()
|
|
554
|
+
}, data, {
|
|
555
|
+
componentId,
|
|
556
|
+
experienceId: experienceId || '',
|
|
557
|
+
variantIndex: variantIndex || 0,
|
|
558
|
+
ctx: this.buildRequestContext()
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const hasComponentViewTrackingThreshold = arg => {
|
|
564
|
+
return typeof arg === 'object' && arg !== null && 'getComponentViewTrackingThreshold' in arg && typeof arg['getComponentViewTrackingThreshold'] === 'function';
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const _excluded = ["experience", "variant", "variantIndex"],
|
|
568
|
+
_excluded2 = ["element"];
|
|
569
|
+
const buildOverrideMiddleware = experienceSelectionMiddleware => _ref => {
|
|
570
|
+
let {
|
|
571
|
+
experience: originalExperience,
|
|
572
|
+
variant: originalVariant,
|
|
573
|
+
variantIndex: originalVariantIndex
|
|
574
|
+
} = _ref,
|
|
575
|
+
other = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
576
|
+
const {
|
|
577
|
+
experience,
|
|
578
|
+
variant,
|
|
579
|
+
variantIndex
|
|
580
|
+
} = experienceSelectionMiddleware({
|
|
581
|
+
experience: originalExperience,
|
|
582
|
+
variant: originalVariant,
|
|
583
|
+
variantIndex: originalVariantIndex
|
|
584
|
+
});
|
|
585
|
+
return Object.assign({}, other, {
|
|
586
|
+
audience: experience != null && experience.audience ? experience.audience : null,
|
|
587
|
+
experience,
|
|
588
|
+
variant,
|
|
589
|
+
variantIndex
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
class Ninetailed {
|
|
593
|
+
constructor(ninetailedApiClientInstanceOrOptions, {
|
|
594
|
+
plugins,
|
|
595
|
+
url,
|
|
596
|
+
locale,
|
|
597
|
+
requestTimeout,
|
|
598
|
+
onLog,
|
|
599
|
+
onError,
|
|
600
|
+
buildClientContext,
|
|
601
|
+
onInitProfileId,
|
|
602
|
+
componentViewTrackingThreshold = 2000,
|
|
603
|
+
storageImpl,
|
|
604
|
+
useClientSideEvaluation = false
|
|
605
|
+
} = {}) {
|
|
606
|
+
var _this = this;
|
|
607
|
+
this.instance = void 0;
|
|
608
|
+
this._profileState = void 0;
|
|
609
|
+
this.isInitialized = false;
|
|
610
|
+
this.apiClient = void 0;
|
|
611
|
+
this.ninetailedCorePlugin = void 0;
|
|
612
|
+
this.elementSeenObserver = void 0;
|
|
613
|
+
this.observedElements = void 0;
|
|
614
|
+
this.clientId = void 0;
|
|
615
|
+
this.environment = void 0;
|
|
616
|
+
this.plugins = void 0;
|
|
617
|
+
this.logger = void 0;
|
|
618
|
+
this.componentViewTrackingThreshold = void 0;
|
|
619
|
+
this.useClientSideEvaluation = void 0;
|
|
620
|
+
this.eventBuilder = void 0;
|
|
621
|
+
this.page = async function (data, options) {
|
|
622
|
+
try {
|
|
623
|
+
const result = PageviewProperties.partial().default({}).safeParse(data);
|
|
624
|
+
if (!result.success) {
|
|
625
|
+
throw new Error(`[Validation Error] "page" was called with invalid params. Page data is not valid: ${result.error.format()}`);
|
|
626
|
+
}
|
|
627
|
+
await _this.waitUntilInitialized();
|
|
628
|
+
await _this.instance.page(data, _this.buildOptions(options));
|
|
629
|
+
return _this.ninetailedCorePlugin.flush();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
logger.error(error);
|
|
632
|
+
if (error instanceof RangeError) {
|
|
633
|
+
throw new Error(`[Validation Error] "page" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
|
|
634
|
+
}
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
this.track = async function (event, properties, options) {
|
|
639
|
+
try {
|
|
640
|
+
const result = Properties.default({}).safeParse(properties);
|
|
641
|
+
if (!result.success) {
|
|
642
|
+
throw new Error(`[Validation Error] "track" was called with invalid params. Properties are no valid json object: ${result.error.format()}`);
|
|
643
|
+
}
|
|
644
|
+
await _this.waitUntilInitialized();
|
|
645
|
+
await _this.instance.track(event.toString(), result.data, _this.buildOptions(options));
|
|
646
|
+
return _this.ninetailedCorePlugin.flush();
|
|
647
|
+
} catch (error) {
|
|
648
|
+
logger.error(error);
|
|
649
|
+
if (error instanceof RangeError) {
|
|
650
|
+
throw new Error(`[Validation Error] "track" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
|
|
651
|
+
}
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
this.identify = async function (uid, traits, options) {
|
|
656
|
+
try {
|
|
657
|
+
const result = Traits.default({}).safeParse(traits);
|
|
658
|
+
if (!result.success) {
|
|
659
|
+
throw new Error(`[Validation Error] "identify" was called with invalid params. Traits are no valid json: ${result.error.format()}`);
|
|
660
|
+
}
|
|
661
|
+
await _this.waitUntilInitialized();
|
|
662
|
+
await _this.instance.identify(uid && uid.toString() !== '' ? uid.toString() : EMPTY_MERGE_ID, result.data, _this.buildOptions(options));
|
|
663
|
+
return _this.ninetailedCorePlugin.flush();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
logger.error(error);
|
|
666
|
+
if (error instanceof RangeError) {
|
|
667
|
+
throw new Error(`[Validation Error] "identify" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
|
|
668
|
+
}
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
this.batch = async function (events) {
|
|
673
|
+
try {
|
|
674
|
+
await _this.waitUntilInitialized();
|
|
675
|
+
const promises = events.map(event => {
|
|
676
|
+
if (isPageViewEvent(event)) {
|
|
677
|
+
return _this.instance.page(event.properties);
|
|
678
|
+
}
|
|
679
|
+
if (isTrackEvent(event)) {
|
|
680
|
+
return _this.instance.track(event.event, event.properties);
|
|
681
|
+
}
|
|
682
|
+
if (isIdentifyEvent(event)) {
|
|
683
|
+
return _this.instance.identify(event.userId || EMPTY_MERGE_ID, event.traits);
|
|
684
|
+
}
|
|
685
|
+
if (isComponentViewEvent(event)) {
|
|
686
|
+
return _this.instance.dispatch({
|
|
687
|
+
experienceId: event.experienceId,
|
|
688
|
+
componentId: event.componentId,
|
|
689
|
+
variantIndex: event.variantIndex,
|
|
690
|
+
type: HAS_SEEN_STICKY_COMPONENT
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return Promise.resolve();
|
|
694
|
+
});
|
|
695
|
+
await Promise.all(promises);
|
|
696
|
+
return _this.ninetailedCorePlugin.flush();
|
|
697
|
+
} catch (error) {
|
|
698
|
+
logger.error(error);
|
|
699
|
+
if (error instanceof RangeError) {
|
|
700
|
+
throw new Error(`[Validation Error] "batch" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
|
|
701
|
+
}
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
this.trackStickyComponentView = async function ({
|
|
706
|
+
experienceId,
|
|
707
|
+
componentId,
|
|
708
|
+
variantIndex
|
|
709
|
+
}) {
|
|
710
|
+
try {
|
|
711
|
+
await _this.waitUntilInitialized();
|
|
712
|
+
await _this.instance.dispatch({
|
|
713
|
+
experienceId,
|
|
714
|
+
componentId,
|
|
715
|
+
variantIndex,
|
|
716
|
+
type: HAS_SEEN_STICKY_COMPONENT
|
|
717
|
+
});
|
|
718
|
+
return _this.ninetailedCorePlugin.flush();
|
|
719
|
+
} catch (error) {
|
|
720
|
+
logger.error(error);
|
|
721
|
+
if (error instanceof RangeError) {
|
|
722
|
+
throw new Error(`[Validation Error] "trackStickyComponentView" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`);
|
|
723
|
+
}
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* @deprecated The legacy datamodel is not recommended anymore
|
|
729
|
+
* Will be removed in the next version of the SDK
|
|
730
|
+
*/
|
|
731
|
+
this.trackHasSeenComponent = async function (properties) {
|
|
732
|
+
return _this.instance.dispatch(Object.assign({}, properties, {
|
|
733
|
+
type: HAS_SEEN_COMPONENT
|
|
734
|
+
}));
|
|
735
|
+
};
|
|
736
|
+
this.trackComponentView = properties => {
|
|
737
|
+
return this.instance.dispatch(Object.assign({}, properties, {
|
|
738
|
+
type: HAS_SEEN_ELEMENT
|
|
739
|
+
}));
|
|
740
|
+
};
|
|
741
|
+
this.observeElement = (payload, options) => {
|
|
742
|
+
const {
|
|
743
|
+
element
|
|
744
|
+
} = payload,
|
|
745
|
+
remaingPayload = _objectWithoutPropertiesLoose(payload, _excluded2);
|
|
746
|
+
if (!(element instanceof Element)) {
|
|
747
|
+
const isObject = typeof element === 'object' && element !== null;
|
|
748
|
+
const constructorName = isObject ? element.constructor.name : '';
|
|
749
|
+
const isConstructorNameNotObject = constructorName && constructorName !== 'Object';
|
|
750
|
+
logger.warn(`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${isConstructorNameNotObject ? ` of type ${constructorName}` : ''}. This call will be ignored.`);
|
|
751
|
+
} else {
|
|
752
|
+
this.observedElements.set(element, remaingPayload);
|
|
753
|
+
const delays = this.pluginsWithCustomComponentViewThreshold.map(plugin => plugin.getComponentViewTrackingThreshold());
|
|
754
|
+
const uniqueDelays = Array.from(new Set([...delays, (options == null ? void 0 : options.delay) || this.componentViewTrackingThreshold]));
|
|
755
|
+
uniqueDelays.forEach(delay => {
|
|
756
|
+
this.elementSeenObserver.observe(element, {
|
|
757
|
+
delay
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
this.unobserveElement = element => {
|
|
763
|
+
this.observedElements.delete(element);
|
|
764
|
+
this.elementSeenObserver.unobserve(element);
|
|
765
|
+
};
|
|
766
|
+
this.onElementSeen = (element, delay) => {
|
|
767
|
+
const payload = this.observedElements.get(element);
|
|
768
|
+
if (typeof payload !== 'undefined') {
|
|
769
|
+
const pluginNamesInterestedInSeenElementMessage = [...this.pluginsWithCustomComponentViewThreshold.filter(plugin => plugin.getComponentViewTrackingThreshold() === delay), ...this.plugins.filter(plugin => !hasComponentViewTrackingThreshold(plugin))].map(plugin => plugin.name);
|
|
770
|
+
if (pluginNamesInterestedInSeenElementMessage.length === 0) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.instance.dispatch(Object.assign({}, payload, {
|
|
774
|
+
element,
|
|
775
|
+
type: HAS_SEEN_ELEMENT,
|
|
776
|
+
plugins: Object.assign({
|
|
777
|
+
all: false
|
|
778
|
+
}, pluginNamesInterestedInSeenElementMessage.reduce((acc, curr) => Object.assign({}, acc, {
|
|
779
|
+
[curr]: true
|
|
780
|
+
}), {}))
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
this.reset = async function () {
|
|
785
|
+
await _this.waitUntilInitialized();
|
|
786
|
+
|
|
787
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
788
|
+
// @ts-ignore
|
|
789
|
+
_this.instance.plugins[PLUGIN_NAME].reset();
|
|
790
|
+
};
|
|
791
|
+
this.debug = async function (enabled) {
|
|
792
|
+
await _this.waitUntilInitialized();
|
|
793
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
794
|
+
// @ts-ignore
|
|
795
|
+
_this.instance.plugins[PLUGIN_NAME].debug(enabled);
|
|
796
|
+
};
|
|
797
|
+
this.onProfileChange = cb => {
|
|
798
|
+
cb(this.profileState);
|
|
799
|
+
return this.instance.on(PROFILE_CHANGE, ({
|
|
800
|
+
payload
|
|
801
|
+
}) => {
|
|
802
|
+
if (payload.error) {
|
|
803
|
+
cb(Object.assign({}, this._profileState, {
|
|
804
|
+
status: 'error',
|
|
805
|
+
profile: payload.profile,
|
|
806
|
+
experiences: payload.experiences,
|
|
807
|
+
error: payload.error
|
|
808
|
+
}));
|
|
809
|
+
} else {
|
|
810
|
+
cb(Object.assign({}, this._profileState, {
|
|
811
|
+
status: 'success',
|
|
812
|
+
profile: payload.profile,
|
|
813
|
+
experiences: payload.experiences,
|
|
814
|
+
error: null
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
};
|
|
819
|
+
this.onSelectVariant = ({
|
|
820
|
+
baseline,
|
|
821
|
+
experiences
|
|
822
|
+
}, cb) => {
|
|
823
|
+
let middlewareChangeListeners = [];
|
|
824
|
+
let state = null;
|
|
825
|
+
const removeMiddlewareChangeListeners = () => {
|
|
826
|
+
middlewareChangeListeners.forEach(removeListener => removeListener());
|
|
827
|
+
middlewareChangeListeners = [];
|
|
828
|
+
};
|
|
829
|
+
const setSelectedVariant = newState => {
|
|
830
|
+
state = newState;
|
|
831
|
+
cb(state);
|
|
832
|
+
};
|
|
833
|
+
const removeProfileChangeListener = this.onProfileChange(profileState => {
|
|
834
|
+
const {
|
|
835
|
+
addListeners,
|
|
836
|
+
removeListeners,
|
|
837
|
+
middleware: experienceSelectionMiddleware
|
|
838
|
+
} = makeExperienceSelectMiddleware({
|
|
839
|
+
plugins: this.plugins,
|
|
840
|
+
experiences,
|
|
841
|
+
baseline,
|
|
842
|
+
profile: profileState.profile,
|
|
843
|
+
onChange: middleware => {
|
|
844
|
+
const overrideResult = buildOverrideMiddleware(middleware);
|
|
845
|
+
if (state !== null) {
|
|
846
|
+
setSelectedVariant(overrideResult(state));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
addListeners();
|
|
851
|
+
middlewareChangeListeners.push(removeListeners);
|
|
852
|
+
const overrideResult = buildOverrideMiddleware(experienceSelectionMiddleware);
|
|
853
|
+
const hasVariants = experiences.map(experience => selectHasVariants(experience, baseline)).reduce((acc, curr) => acc || curr, false);
|
|
854
|
+
const baseReturn = Object.assign({}, profileState, {
|
|
855
|
+
hasVariants,
|
|
856
|
+
baseline
|
|
857
|
+
});
|
|
858
|
+
const emptyReturn = Object.assign({}, baseReturn, {
|
|
859
|
+
experience: null,
|
|
860
|
+
variant: baseline,
|
|
861
|
+
variantIndex: 0,
|
|
862
|
+
audience: null,
|
|
863
|
+
isPersonalized: false,
|
|
864
|
+
profile: null,
|
|
865
|
+
error: null
|
|
866
|
+
});
|
|
867
|
+
if (profileState.status === 'loading') {
|
|
868
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
869
|
+
loading: true,
|
|
870
|
+
status: 'loading'
|
|
871
|
+
})));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (profileState.status === 'error') {
|
|
875
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
876
|
+
loading: false,
|
|
877
|
+
status: 'error',
|
|
878
|
+
error: profileState.error
|
|
879
|
+
})));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const {
|
|
883
|
+
profile,
|
|
884
|
+
experiences: selectedExperiences
|
|
885
|
+
} = profileState;
|
|
886
|
+
if (!profile || !selectedExperiences) {
|
|
887
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
888
|
+
loading: false,
|
|
889
|
+
status: 'error',
|
|
890
|
+
error: new Error('No Profile or Selected Experiences were returned by the API')
|
|
891
|
+
})));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (this.useClientSideEvaluation) {
|
|
895
|
+
const _experience = selectExperience({
|
|
896
|
+
experiences,
|
|
897
|
+
profile
|
|
898
|
+
});
|
|
899
|
+
if (!_experience) {
|
|
900
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
901
|
+
loading: false,
|
|
902
|
+
status: 'success',
|
|
903
|
+
profile
|
|
904
|
+
})));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const {
|
|
908
|
+
variant: _variant,
|
|
909
|
+
index
|
|
910
|
+
} = selectVariant$1({
|
|
911
|
+
baseline,
|
|
912
|
+
experience: _experience,
|
|
913
|
+
profile
|
|
914
|
+
});
|
|
915
|
+
setSelectedVariant(overrideResult(Object.assign({}, baseReturn, {
|
|
916
|
+
status: 'success',
|
|
917
|
+
loading: false,
|
|
918
|
+
error: null,
|
|
919
|
+
experience: _experience,
|
|
920
|
+
variant: _variant,
|
|
921
|
+
variantIndex: index,
|
|
922
|
+
audience: _experience.audience ? _experience.audience : null,
|
|
923
|
+
profile,
|
|
924
|
+
isPersonalized: true
|
|
925
|
+
})));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const experience = experiences.find(experience => selectedExperiences.some(selectedExperience => selectedExperience.experienceId === experience.id));
|
|
929
|
+
const selectedExperience = selectedExperiences.find(({
|
|
930
|
+
experienceId
|
|
931
|
+
}) => experienceId === (experience == null ? void 0 : experience.id));
|
|
932
|
+
if (!experience || !selectedExperience) {
|
|
933
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
934
|
+
loading: false,
|
|
935
|
+
status: 'success',
|
|
936
|
+
profile
|
|
937
|
+
})));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const baselineVariants = selectBaselineWithVariants(experience, baseline);
|
|
941
|
+
if (!baselineVariants) {
|
|
942
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
943
|
+
loading: false,
|
|
944
|
+
status: 'success',
|
|
945
|
+
profile
|
|
946
|
+
})));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const {
|
|
950
|
+
variants
|
|
951
|
+
} = baselineVariants;
|
|
952
|
+
const variant = variants[selectedExperience.variantIndex - 1];
|
|
953
|
+
if (!variant) {
|
|
954
|
+
setSelectedVariant(overrideResult(Object.assign({}, emptyReturn, {
|
|
955
|
+
loading: false,
|
|
956
|
+
status: 'success',
|
|
957
|
+
profile
|
|
958
|
+
})));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
setSelectedVariant(overrideResult(Object.assign({}, baseReturn, {
|
|
962
|
+
status: 'success',
|
|
963
|
+
loading: false,
|
|
964
|
+
error: null,
|
|
965
|
+
experience,
|
|
966
|
+
variant,
|
|
967
|
+
variantIndex: selectedExperience.variantIndex,
|
|
968
|
+
audience: experience.audience ? experience.audience : null,
|
|
969
|
+
profile,
|
|
970
|
+
isPersonalized: true
|
|
971
|
+
})));
|
|
972
|
+
});
|
|
973
|
+
return () => {
|
|
974
|
+
removeProfileChangeListener();
|
|
975
|
+
removeMiddlewareChangeListeners();
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
this.onIsInitialized = onIsInitialized => {
|
|
979
|
+
if (typeof onIsInitialized === 'function') {
|
|
980
|
+
if (this.isInitialized) {
|
|
981
|
+
onIsInitialized();
|
|
982
|
+
} else {
|
|
983
|
+
const detachOnReadyListener = this.instance.on('ready', () => {
|
|
984
|
+
this.isInitialized = true;
|
|
985
|
+
onIsInitialized();
|
|
986
|
+
detachOnReadyListener();
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
this.waitUntilInitialized = () => {
|
|
992
|
+
return new Promise(resolve => {
|
|
993
|
+
this.onIsInitialized(resolve);
|
|
994
|
+
});
|
|
995
|
+
};
|
|
996
|
+
this.onVisibilityChange = () => {
|
|
997
|
+
if (typeof document === 'undefined') {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
document.addEventListener('visibilitychange', () => {
|
|
1001
|
+
if (document.visibilityState === 'hidden') {
|
|
1002
|
+
this.instance.dispatch({
|
|
1003
|
+
type: PAGE_HIDDEN
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
};
|
|
1008
|
+
this.useClientSideEvaluation = useClientSideEvaluation;
|
|
1009
|
+
if (ninetailedApiClientInstanceOrOptions instanceof NinetailedApiClient) {
|
|
1010
|
+
this.apiClient = ninetailedApiClientInstanceOrOptions;
|
|
1011
|
+
} else {
|
|
1012
|
+
const {
|
|
1013
|
+
clientId,
|
|
1014
|
+
environment,
|
|
1015
|
+
preview
|
|
1016
|
+
} = ninetailedApiClientInstanceOrOptions;
|
|
1017
|
+
this.clientId = clientId;
|
|
1018
|
+
this.environment = environment || 'main';
|
|
1019
|
+
this.apiClient = new NinetailedApiClient({
|
|
1020
|
+
clientId,
|
|
1021
|
+
environment,
|
|
1022
|
+
url,
|
|
1023
|
+
preview
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
this.plugins = (plugins != null ? plugins : []).flat();
|
|
1027
|
+
this.plugins.forEach(plugin => {
|
|
1028
|
+
if (acceptsCredentials(plugin) && this.clientId && this.environment) {
|
|
1029
|
+
plugin.setCredentials({
|
|
1030
|
+
clientId: this.clientId,
|
|
1031
|
+
environment: this.environment
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
this._profileState = {
|
|
1036
|
+
status: 'loading',
|
|
1037
|
+
profile: null,
|
|
1038
|
+
experiences: null,
|
|
1039
|
+
error: null,
|
|
1040
|
+
from: 'api'
|
|
1041
|
+
};
|
|
1042
|
+
if (typeof onLog === 'function') {
|
|
1043
|
+
logger.addSink(new OnLogLogSink(onLog));
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof onError === 'function') {
|
|
1046
|
+
logger.addSink(new OnErrorLogSink(onError));
|
|
1047
|
+
}
|
|
1048
|
+
this.eventBuilder = new EventBuilder(buildClientContext);
|
|
1049
|
+
this.logger = logger;
|
|
1050
|
+
this.ninetailedCorePlugin = new NinetailedCorePlugin({
|
|
1051
|
+
apiClient: this.apiClient,
|
|
1052
|
+
locale,
|
|
1053
|
+
requestTimeout,
|
|
1054
|
+
buildClientContext,
|
|
1055
|
+
onInitProfileId,
|
|
1056
|
+
ninetailed: this
|
|
1057
|
+
});
|
|
1058
|
+
this.instance = Analytics(Object.assign({
|
|
1059
|
+
app: 'ninetailed',
|
|
1060
|
+
plugins: [...this.plugins, this.ninetailedCorePlugin]
|
|
1061
|
+
}, storageImpl ? {
|
|
1062
|
+
storage: storageImpl
|
|
1063
|
+
} : {}));
|
|
1064
|
+
const _detachOnReadyListener = this.instance.on('ready', () => {
|
|
1065
|
+
this.isInitialized = true;
|
|
1066
|
+
logger.info('Ninetailed Experience.js SDK is completely initialized.');
|
|
1067
|
+
_detachOnReadyListener();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// put in private method
|
|
1071
|
+
this.onProfileChange(profileState => {
|
|
1072
|
+
this._profileState = profileState;
|
|
1073
|
+
if (typeof window !== 'undefined') {
|
|
1074
|
+
window.ninetailed = Object.assign({}, window.ninetailed, {
|
|
1075
|
+
profile: this.profileState.profile,
|
|
1076
|
+
experiences: this.profileState.experiences
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
this.observedElements = new WeakMap();
|
|
1081
|
+
this.elementSeenObserver = new ElementSeenObserver({
|
|
1082
|
+
onElementSeen: this.onElementSeen.bind(this)
|
|
1083
|
+
});
|
|
1084
|
+
this.componentViewTrackingThreshold = componentViewTrackingThreshold;
|
|
1085
|
+
const hasPluginsInterestedInHiddenPage = this.plugins.some(isInterestedInHiddenPage);
|
|
1086
|
+
if (hasPluginsInterestedInHiddenPage) {
|
|
1087
|
+
this.onVisibilityChange();
|
|
1088
|
+
}
|
|
1089
|
+
this.registerWindowHandlers();
|
|
1090
|
+
}
|
|
1091
|
+
get pluginsWithCustomComponentViewThreshold() {
|
|
1092
|
+
return [this.ninetailedCorePlugin, ...this.plugins].filter(plugin => hasComponentViewTrackingThreshold(plugin));
|
|
1093
|
+
}
|
|
1094
|
+
get profileState() {
|
|
1095
|
+
return this._profileState;
|
|
1096
|
+
}
|
|
1097
|
+
buildOptions(options = {}) {
|
|
1098
|
+
return Object.assign({}, options);
|
|
1099
|
+
}
|
|
1100
|
+
registerWindowHandlers() {
|
|
1101
|
+
if (typeof window !== 'undefined') {
|
|
1102
|
+
window.ninetailed = Object.assign({}, window.ninetailed, {
|
|
1103
|
+
page: this.page.bind(this),
|
|
1104
|
+
track: this.track.bind(this),
|
|
1105
|
+
identify: this.identify.bind(this),
|
|
1106
|
+
reset: this.reset.bind(this),
|
|
1107
|
+
debug: this.debug.bind(this),
|
|
1108
|
+
profile: this.profileState.profile
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const selectVariant = (baseline, variants, {
|
|
1115
|
+
status,
|
|
1116
|
+
profile,
|
|
1117
|
+
error
|
|
1118
|
+
}, options = {
|
|
1119
|
+
holdout: -1
|
|
1120
|
+
}) => {
|
|
1121
|
+
if (status === 'loading') {
|
|
1122
|
+
return {
|
|
1123
|
+
loading: true,
|
|
1124
|
+
variant: Object.assign({}, baseline, {
|
|
1125
|
+
id: 'baseline',
|
|
1126
|
+
audience: {
|
|
1127
|
+
id: 'baseline'
|
|
1128
|
+
}
|
|
1129
|
+
}),
|
|
1130
|
+
audience: {
|
|
1131
|
+
id: 'baseline'
|
|
1132
|
+
},
|
|
1133
|
+
isPersonalized: false,
|
|
1134
|
+
error: null
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
if (status === 'error') {
|
|
1138
|
+
return {
|
|
1139
|
+
loading: false,
|
|
1140
|
+
variant: Object.assign({}, baseline, {
|
|
1141
|
+
id: 'baseline',
|
|
1142
|
+
audience: {
|
|
1143
|
+
id: 'baseline'
|
|
1144
|
+
}
|
|
1145
|
+
}),
|
|
1146
|
+
audience: {
|
|
1147
|
+
id: 'baseline'
|
|
1148
|
+
},
|
|
1149
|
+
isPersonalized: false,
|
|
1150
|
+
error: error
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
const variant = variants.find(variant => {
|
|
1154
|
+
var _profile$audiences, _variant$audience;
|
|
1155
|
+
return profile == null || (_profile$audiences = profile.audiences) == null ? void 0 : _profile$audiences.includes((_variant$audience = variant.audience) == null ? void 0 : _variant$audience.id);
|
|
1156
|
+
});
|
|
1157
|
+
if (variant) {
|
|
1158
|
+
if (options != null && options.holdout || -1 > ((profile == null ? void 0 : profile.random) || 0)) {
|
|
1159
|
+
return {
|
|
1160
|
+
loading: false,
|
|
1161
|
+
variant: Object.assign({}, baseline, {
|
|
1162
|
+
audience: {
|
|
1163
|
+
id: 'baseline'
|
|
1164
|
+
}
|
|
1165
|
+
}),
|
|
1166
|
+
audience: Object.assign({}, variant.audience, {
|
|
1167
|
+
id: variant.audience.id
|
|
1168
|
+
}),
|
|
1169
|
+
isPersonalized: false,
|
|
1170
|
+
error: null
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
loading: false,
|
|
1175
|
+
variant,
|
|
1176
|
+
audience: Object.assign({}, variant.audience, {
|
|
1177
|
+
id: variant.audience.id
|
|
1178
|
+
}),
|
|
1179
|
+
isPersonalized: true,
|
|
1180
|
+
error: null
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* There was no matching audience found.
|
|
1186
|
+
*/
|
|
1187
|
+
return {
|
|
1188
|
+
loading: false,
|
|
1189
|
+
variant: Object.assign({}, baseline, {
|
|
1190
|
+
id: 'baseline',
|
|
1191
|
+
audience: {
|
|
1192
|
+
id: 'baseline'
|
|
1193
|
+
}
|
|
1194
|
+
}),
|
|
1195
|
+
audience: {
|
|
1196
|
+
id: 'baseline'
|
|
1197
|
+
},
|
|
1198
|
+
isPersonalized: false,
|
|
1199
|
+
error: null
|
|
1200
|
+
};
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
export { ANONYMOUS_ID, COMPONENT, COMPONENT_START, CONSENT, DEBUG_FLAG, EMPTY_MERGE_ID, EXPERIENCES_FALLBACK_CACHE, HAS_SEEN_STICKY_COMPONENT, LEGACY_ANONYMOUS_ID, Ninetailed, NinetailedCorePlugin, OnChangeEmitter, PAGE_HIDDEN, PLUGIN_NAME, PROFILE_CHANGE, PROFILE_FALLBACK_CACHE, PROFILE_RESET, SET_ENABLED_FEATURES, buildClientNinetailedRequestContext, decodeExperienceVariantsMap, makeExperienceSelectMiddleware, selectPluginsHavingExperienceSelectionMiddleware, selectPluginsHavingOnChangeEmitter, selectVariant };
|