-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathbrowser.js
More file actions
418 lines (370 loc) · 15 KB
/
Copy pathbrowser.js
File metadata and controls
418 lines (370 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/* eslint-disable no-underscore-dangle */
/* eslint-disable class-methods-use-this */
import { NAME, DISPLAY_NAME, AMPLITUDE_SDK_V2 } from './constants';
import { isDefinedAndNotNullAndNotEmpty } from '../../utils/commonUtils';
import Logger from '../../utils/logger';
import { loadNativeSdkV1, loadNativeSdkV2 } from './nativeSdkLoader';
import {
getTraitsToSetOnce,
getTraitsToIncrement,
getDestinationOptions,
getFieldsToUnset,
formatUrl,
getAmplitudeSdkVersion,
getAutoCapturePageViews,
getPageUrlEnrichment,
getTrackSessionEvents,
getWebVitals,
getFileDownloads,
getFrustrationInteractions,
getNetworkTracking,
getElementInteractions,
getFormInteractions,
} from './utils';
import { getValueOrDefault } from '../../utils/utils';
const logger = new Logger(DISPLAY_NAME);
class Amplitude {
constructor(config, analytics, destinationInfo) {
if (analytics.logLevel) {
logger.setLogLevel(analytics.logLevel);
}
this.name = NAME;
this.analytics = analytics;
this.apiKey = config.apiKey;
this.proxyServerUrl = config.proxyServerUrl;
this.residencyServer = config.residencyServer;
this.trackAllPages = config.trackAllPages || false;
this.trackNamedPages = config.trackNamedPages || false;
this.trackCategorizedPages = config.trackCategorizedPages || false;
this.attribution = config.attribution || false;
this.flushQueueSize = config.eventUploadThreshold || 30;
this.flushIntervalMillis = +config.eventUploadPeriodMillis || 1000;
this.trackNewCampaigns = getValueOrDefault(config.trackNewCampaigns, true);
this.trackRevenuePerProduct = config.trackRevenuePerProduct || false;
this.preferAnonymousIdForDeviceId = config.preferAnonymousIdForDeviceId || false;
this.traitsToSetOnce = getTraitsToSetOnce(config);
this.traitsToIncrement = getTraitsToIncrement(config);
this.appendFieldsToEventProps = config.appendFieldsToEventProps || false;
this.unsetParamsReferrerOnNewSession = config.unsetParamsReferrerOnNewSession || false;
this.trackProductsOnce = config.trackProductsOnce || false;
this.versionName = config.versionName;
this.groupTypeTrait = config.groupTypeTrait;
this.groupValueTrait = config.groupValueTrait;
this.sdkVersion = getAmplitudeSdkVersion(config);
this.autoCapturePageViews = getAutoCapturePageViews(config);
this.pageUrlEnrichment = getPageUrlEnrichment(config);
this.trackSessionEvents = getTrackSessionEvents(config);
this.webVitals = getWebVitals(config);
this.fileDownloads = getFileDownloads(config);
this.frustrationInteractions = getFrustrationInteractions(config);
this.networkTracking = getNetworkTracking(config);
this.elementInteractions = getElementInteractions(config);
this.formInteractions = getFormInteractions(config);
({
shouldApplyDeviceModeTransformation: this.shouldApplyDeviceModeTransformation,
propagateEventsUntransformedOnError: this.propagateEventsUntransformedOnError,
destinationId: this.destinationId,
} = destinationInfo ?? {});
}
init() {
if (this.analytics.loadIntegration) {
if (this.sdkVersion === AMPLITUDE_SDK_V2) {
loadNativeSdkV2(window, document);
} else {
loadNativeSdkV1(window, document);
}
}
const commonInitOptions = {
flushQueueSize: this.flushQueueSize,
flushIntervalMillis: this.flushIntervalMillis,
appVersion: this.versionName,
};
const initOptions =
this.sdkVersion === AMPLITUDE_SDK_V2
? {
...commonInitOptions,
autocapture: {
attribution: !this.attribution,
pageViews: this.autoCapturePageViews,
sessions: this.trackSessionEvents,
formInteractions: this.formInteractions,
fileDownloads: this.fileDownloads,
elementInteractions: this.elementInteractions,
frustrationInteractions: this.frustrationInteractions,
networkTracking: this.networkTracking,
webVitals: this.webVitals,
pageUrlEnrichment: this.pageUrlEnrichment,
},
}
: {
...commonInitOptions,
attribution: { disabled: this.attribution, trackNewCampaigns: !this.trackNewCampaigns },
};
if (isDefinedAndNotNullAndNotEmpty(this.proxyServerUrl)) {
if (this.proxyServerUrl.startsWith('http://')) {
logger.error(`Please use a secure proxy server URL: ${this.proxyServerUrl}`);
} else {
initOptions.serverUrl = formatUrl(this.proxyServerUrl);
}
}
// EU data residency
// Relevant doc: https://www.docs.developers.amplitude.com/data/sdks/typescript-browser/#eu-data-residency
if (this.residencyServer === 'EU') {
initOptions.serverZone = 'EU';
}
if (
navigator.userAgent.indexOf('MSIE') !== -1 ||
navigator.appVersion.indexOf('Trident/') > -1
) {
initOptions.transport = 'xhr';
}
if (this.preferAnonymousIdForDeviceId && this.analytics)
initOptions.deviceId = this.analytics.getAnonymousId();
// v2's init expects an absent (undefined) userId; v1 keeps the existing null.
const userId = this.sdkVersion === AMPLITUDE_SDK_V2 ? undefined : null;
window.amplitude.init(this.apiKey, userId, initOptions);
}
isLoaded() {
return Boolean(window.amplitude?.getDeviceId());
}
isReady() {
return this.isLoaded();
}
identify(rudderElement) {
this.setDeviceId(rudderElement);
// rudderElement.message.context will always be present as part of identify event payload.
const { traits } = rudderElement.message.context;
const { userId, integrations } = rudderElement.message;
const fieldsToUnset = getFieldsToUnset(integrations);
const amplitudeIdentify = new window.amplitude.Identify();
let sendIdentifyCall = false;
if (fieldsToUnset) {
sendIdentifyCall = true;
// AM Docs: https://amplitude.github.io/Amplitude-JavaScript/Identify/#identifyunset
fieldsToUnset.forEach(fieldToUnset => {
amplitudeIdentify.unset(fieldToUnset);
});
}
if (userId) {
window.amplitude.setUserId(userId);
}
if (traits) {
sendIdentifyCall = true;
Object.keys(traits).forEach(trait => {
const shouldIncrement = this.traitsToIncrement.includes(trait);
const shouldSetOnce = this.traitsToSetOnce.includes(trait);
if (shouldIncrement) {
amplitudeIdentify.add(trait, traits[trait]);
}
if (shouldSetOnce) {
amplitudeIdentify.setOnce(trait, traits[trait]);
}
if (!shouldIncrement && !shouldSetOnce) {
amplitudeIdentify.set(trait, traits[trait]);
}
});
}
if (sendIdentifyCall) {
window.amplitude.identify(amplitudeIdentify);
}
}
track(rudderElement) {
this.setDeviceId(rudderElement);
const { properties } = rudderElement.message;
// message.properties will always be present as part of track event.
const { products } = properties;
const clonedTrackEvent = {};
Object.assign(clonedTrackEvent, rudderElement.message);
// For track products once, we will send the products in a single call.
if (this.trackProductsOnce) {
if (products && Array.isArray(products)) {
// track all the products in a single event.
const allProducts = [];
const productKeys = Object.keys(products);
for (let index = 0; index < productKeys.length; index += 1) {
let product = {};
product = this.getProductAttributes(products[index]);
allProducts.push(product);
}
clonedTrackEvent.properties.products = allProducts;
this.logEventAndCorrespondingRevenue(clonedTrackEvent, this.trackRevenuePerProduct); // we do not want to track revenue as a whole if trackRevenuePerProduct is enabled.
// If trackRevenuePerProduct is enabled, track revenues per product.
if (this.trackRevenuePerProduct) {
const trackEventMessage = {};
Object.assign(trackEventMessage, clonedTrackEvent);
this.trackingEventAndRevenuePerProduct(trackEventMessage, products, false); // also track revenue only and not event per product.
}
} else {
// track event and revenue as a whole as products array is not available.
this.logEventAndCorrespondingRevenue(clonedTrackEvent, false);
}
return;
}
if (products && Array.isArray(products)) {
// track events iterating over product array individually.
// Log the actuall event without products array. We will subsequently track each product with 'Product Purchased' event.
delete clonedTrackEvent.properties.products;
this.logEventAndCorrespondingRevenue(clonedTrackEvent, this.trackRevenuePerProduct);
const trackEventMessage = {};
Object.assign(trackEventMessage, clonedTrackEvent);
// track products and revenue per product basis.
this.trackingEventAndRevenuePerProduct(trackEventMessage, products, true); // track both event and revenue on per product basis.
} else {
// track event and revenue as a whole as no product array is present.
this.logEventAndCorrespondingRevenue(clonedTrackEvent, false);
}
}
trackingEventAndRevenuePerProduct(trackEventMessage, products, shouldTrackEventPerProduct) {
const eventMessage = trackEventMessage;
let { revenueType } = eventMessage.properties;
const { revenue, revenue_type: revenueTtype } = eventMessage.properties;
revenueType = revenueType || revenueTtype;
products.forEach(product => {
eventMessage.properties = product;
eventMessage.event = 'Product Purchased';
if (this.trackRevenuePerProduct) {
if (revenueType) {
eventMessage.properties.revenueType = revenueType;
}
if (revenue) {
eventMessage.properties.revenue = revenue;
}
this.trackRevenue(eventMessage);
}
if (shouldTrackEventPerProduct) {
this.logEventAndCorrespondingRevenue(eventMessage, true);
}
});
}
// Always to be called for general and top level events (and not product level)
// For these events we expect top level revenue property.
logEventAndCorrespondingRevenue(rudderMessage, dontTrackRevenue) {
const { properties, event } = rudderMessage;
window.amplitude.track(event, properties);
if (properties.revenue && !dontTrackRevenue) {
this.trackRevenue(rudderMessage);
}
}
/**
* track page events base on destination settings. If more than one settings is enabled, multiple events may be logged for a single page event.
* For example, if category of a page is present, and both trackAllPages and trackCategorizedPages are enabled, then 2 events will be tracked for
* a single pageview - 'Loaded a page' and `Viewed page ${category}`.
*
* @memberof Amplitude
*/
page(rudderElement) {
this.setDeviceId(rudderElement);
const { properties, name, category, integrations } = rudderElement.message;
const amplitudeIntgConfig = getDestinationOptions(integrations);
const useNewPageEventNameFormat = amplitudeIntgConfig?.useNewPageEventNameFormat || false;
// all pages
if (this.trackAllPages) {
const event = 'Loaded a page';
window.amplitude.track(event, properties);
}
// categorized pages
if (category && this.trackCategorizedPages) {
let event;
if (!useNewPageEventNameFormat) event = `Viewed page ${category}`;
else event = `Viewed ${category} Page`;
window.amplitude.track(event, properties);
}
// named pages
if (name && this.trackNamedPages) {
let event;
if (!useNewPageEventNameFormat) event = `Viewed page ${name}`;
else event = `Viewed ${name} Page`;
window.amplitude.track(event, properties);
}
}
group(rudderElement) {
this.setDeviceId(rudderElement);
const { groupId, traits } = rudderElement.message;
const { groupTypeTrait } = this;
const { groupValueTrait } = this;
let groupType;
let groupValue;
if (groupTypeTrait && groupValueTrait && traits) {
groupType = traits[groupTypeTrait];
groupValue = traits[groupValueTrait];
}
if (groupType && groupValue) {
window.amplitude.setGroup(groupTypeTrait, groupValueTrait);
} else if (groupId) {
// Similar as segment but not sure whether we need it as our cloud mode supports only the above if block
window.amplitude.setGroup('[Rudderstack] Group', groupId);
}
// https://developers.amplitude.com/docs/setting-user-properties#setting-group-properties
// no other api for setting group properties for javascript
}
setDeviceId(rudderElement) {
const { anonymousId } = rudderElement.message;
if (this.preferAnonymousIdForDeviceId && anonymousId) {
window.amplitude.setDeviceId(anonymousId);
}
}
/**
* Tracks revenue with logRevenueV2() api based on revenue/price present in event payload. If neither of revenue/price present, it returns.
* The event payload may contain ruddermessage of an original track event payload (from trackEvent method) or it is derived from a product
* array (from trackingRevenuePerProduct) in an e-comm event.
*
* @param {*} rudderMessage
* @returns
* @memberof Amplitude
*/
trackRevenue(rudderMessage) {
const mapRevenueType = {
'order completed': 'Purchase',
'completed order': 'Purchase',
'product purchased': 'Purchase',
};
const { properties, event } = rudderMessage;
let { price, productId, quantity } = properties;
const { revenue, product_id: pId, revenue_type: revenueTtype } = properties;
const revenueType =
properties.revenueType || revenueTtype || mapRevenueType[event.toLowerCase()];
productId = productId || pId;
// If neither revenue nor price is present, then return
// else send price and quantity from properties to amplitude
// If price not present set price as revenue's value and force quantity to be 1.
// Ultimately set quantity to 1 if not already present from above logic.
if (!revenue && !price) {
logger.error('Neither "revenue" nor "price" is available. Hence, aborting');
return;
}
if (!price) {
price = revenue;
quantity = 1;
}
if (!quantity) {
quantity = 1;
}
const amplitudeRevenue = new window.amplitude.Revenue()
.setPrice(price)
.setQuantity(quantity)
.setEventProperties(properties);
if (revenueType) {
amplitudeRevenue.setRevenueType(revenueType);
}
if (productId) {
amplitudeRevenue.setProductId(productId);
}
if (amplitudeRevenue._properties) {
delete amplitudeRevenue._properties.price;
delete amplitudeRevenue._properties.productId;
delete amplitudeRevenue._properties.quantity;
}
window.amplitude.revenue(amplitudeRevenue);
}
getProductAttributes(product) {
return {
productId: product.productId || product.product_id,
sku: product.sku,
name: product.name,
price: product.price,
quantity: product.quantity,
category: product.category,
};
}
}
export default Amplitude;