@adobe/spacecat-shared-rum-api-client 2.3.0 → 2.5.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/common/traffic.js +128 -0
- package/src/functions/experiment.js +40 -2
- package/src/functions/traffic-acquisition.js +73 -0
- package/src/index.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.5.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.4.0...@adobe/spacecat-shared-rum-api-client-v2.5.0) (2024-07-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Experimentation entity ([#288](https://github.com/adobe/spacecat-shared/issues/288)) ([774e2c7](https://github.com/adobe/spacecat-shared/commit/774e2c7013c9e617c745c494e20e1cdd8cce71e7))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.4.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.3.0...@adobe/spacecat-shared-rum-api-client-v2.4.0) (2024-07-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* traffic acquisition detection ([#286](https://github.com/adobe/spacecat-shared/issues/286)) ([b3d1f1c](https://github.com/adobe/spacecat-shared/commit/b3d1f1caa288ed5cdda367b64dea24886cf87afb))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-rum-api-client-v2.3.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.2.1...@adobe/spacecat-shared-rum-api-client-v2.3.0) (2024-07-12)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
/* eslint-disable object-curly-newline */
|
|
13
|
+
|
|
14
|
+
import { hasText } from '@adobe/spacecat-shared-utils';
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* --------- DEFINITIONS ----------------
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Referrer related
|
|
21
|
+
// matches second level domains 1:1 ignoring subdomains and top-level domains
|
|
22
|
+
// for example: https://l.instagram.com matches, whereas https://wwww.linstagram.com does not
|
|
23
|
+
const searchEngines = /^(https?:\/\/)?(.*\.)?(google|yahoo|bing|yandex|baidu|duckduckgo|brave|ecosia|aol|startpage|ask)\.(.*)(\/|$)/;
|
|
24
|
+
const socialMedias = /^(https?:\/\/)?(.*\.)?(facebook|tiktok|snapchat|x|twitter|pinterest|reddit|linkedin|threads|quora|discord|tumblr|mastodon|bluesky|instagram)\.(.*)(\/|$)/;
|
|
25
|
+
const adNetworks = /googlesyndication|2mdn/;
|
|
26
|
+
const videoPlatforms = /^(https?:\/\/)?(.*\.)?(youtube|vimeo|twitch|dailymotion|wistia)\.(.*)(\/|$)/;
|
|
27
|
+
|
|
28
|
+
// UTM Source related
|
|
29
|
+
const paidDisplaySources = ['gdn'];
|
|
30
|
+
|
|
31
|
+
// UTM Medium related
|
|
32
|
+
// matches 'pp', *cp[acmuv]*, *ppc*, *paid*
|
|
33
|
+
const paidUTMMediums = /^\bpp\b|(.*(cp[acmuv]|ppc|paid|display|banner|poster|placement).*)$/;
|
|
34
|
+
const searchEngineUTMMediums = ['google', 'paidsearch', 'paidsearchnb', 'sea', 'sem'];
|
|
35
|
+
const socialMediaUTMMediums = ['facebook', 'gnews', 'instagramfeed', 'instagramreels', 'instagramstories', 'line', 'linkedin', 'metasearch', 'organicsocialown', 'paidsocial', 'social', 'sociallinkedin', 'socialpaid'];
|
|
36
|
+
const affiliateUTMMediums = ['aff', 'affiliate', 'affiliatemarketing'];
|
|
37
|
+
const organicUTMMediums = ['organicsocial'];
|
|
38
|
+
const emailUTMMediums = ['em', 'email', 'mail', 'newsletter'];
|
|
39
|
+
const smsUTMMediums = ['sms', 'mms'];
|
|
40
|
+
const qrUTMMediums = ['qr', 'qrcode'];
|
|
41
|
+
const pushUTMMediums = ['push', 'pushnotification'];
|
|
42
|
+
|
|
43
|
+
// Tracking params - based on the checkpoints we have in rum-enhancer now
|
|
44
|
+
// const organicTrackingParams = ['srsltid']; WE DO NOT HAVE THIS AS OF NOW
|
|
45
|
+
const paidTrackingParams = ['paid'];
|
|
46
|
+
const emailTrackingParams = ['email'];
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* --------- HELPERS ----------------
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const any = () => true;
|
|
53
|
+
|
|
54
|
+
const anyOf = (truth) => (text) => {
|
|
55
|
+
if (Array.isArray(truth)) return truth.includes(text);
|
|
56
|
+
if (truth instanceof RegExp) return truth.test(text);
|
|
57
|
+
return truth === text;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const none = (input) => (Array.isArray(input) ? input.length === 0 : !hasText(input));
|
|
61
|
+
|
|
62
|
+
const not = (truth) => (text) => {
|
|
63
|
+
if (!hasText(text)) return false;
|
|
64
|
+
if (Array.isArray(truth)) return !truth.includes(text);
|
|
65
|
+
if (truth instanceof RegExp) return !truth.test(text);
|
|
66
|
+
return truth !== text;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const notEmpty = (text) => hasText(text);
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* --------- RULES ----------------
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
// ORDER IS IMPORTANT
|
|
76
|
+
const RULES = (origin) => ([
|
|
77
|
+
// PAID
|
|
78
|
+
{ type: 'paid', category: 'search', referrer: anyOf(searchEngines), utmSource: any, utmMedium: anyOf(searchEngineUTMMediums), tracking: none },
|
|
79
|
+
{ type: 'paid', category: 'search', referrer: anyOf(searchEngines), utmSource: any, utmMedium: any, tracking: anyOf(paidTrackingParams) },
|
|
80
|
+
{ type: 'paid', category: 'social', referrer: anyOf(socialMedias), utmSource: any, utmMedium: anyOf(socialMediaUTMMediums), tracking: none },
|
|
81
|
+
{ type: 'paid', category: 'social', referrer: anyOf(socialMedias), utmSource: any, utmMedium: any, tracking: anyOf(paidTrackingParams) },
|
|
82
|
+
{ type: 'paid', category: 'video', referrer: anyOf(videoPlatforms), utmSource: any, utmMedium: anyOf(paidUTMMediums), tracking: any },
|
|
83
|
+
{ type: 'paid', category: 'video', referrer: anyOf(videoPlatforms), utmSource: any, utmMedium: any, tracking: anyOf(paidTrackingParams) },
|
|
84
|
+
{ type: 'paid', category: 'display', referrer: notEmpty, utmSource: any, utmMedium: anyOf(paidUTMMediums), tracking: any },
|
|
85
|
+
{ type: 'paid', category: 'display', referrer: anyOf(adNetworks), utmSource: any, utmMedium: any, tracking: any },
|
|
86
|
+
{ type: 'paid', category: 'display', referrer: notEmpty, utmSource: anyOf(paidDisplaySources), utmMedium: any, tracking: any },
|
|
87
|
+
{ type: 'paid', category: 'affiliate', referrer: notEmpty, utmSource: any, utmMedium: anyOf(affiliateUTMMediums), tracking: any },
|
|
88
|
+
{ type: 'paid', category: 'uncategorized', referrer: not(origin), utmSource: any, utmMedium: anyOf(paidUTMMediums), tracking: any },
|
|
89
|
+
{ type: 'paid', category: 'uncategorized', referrer: not(origin), utmSource: any, utmMedium: any, tracking: anyOf(paidTrackingParams) },
|
|
90
|
+
|
|
91
|
+
// EARNED
|
|
92
|
+
{ type: 'earned', category: 'search', referrer: anyOf(searchEngines), utmSource: none, utmMedium: none, tracking: none },
|
|
93
|
+
{ type: 'earned', category: 'search', referrer: anyOf(searchEngines), utmSource: any, utmMedium: not(paidUTMMediums), tracking: not(paidTrackingParams) },
|
|
94
|
+
{ type: 'earned', category: 'social', referrer: anyOf(socialMedias), utmSource: none, utmMedium: none, tracking: none },
|
|
95
|
+
{ type: 'earned', category: 'social', referrer: not(origin), utmSource: any, utmMedium: anyOf(organicUTMMediums), tracking: none },
|
|
96
|
+
{ type: 'earned', category: 'video', referrer: anyOf(videoPlatforms), utmSource: none, utmMedium: none, tracking: none },
|
|
97
|
+
{ type: 'earned', category: 'video', referrer: anyOf(videoPlatforms), utmSource: any, utmMedium: not(paidUTMMediums), tracking: none },
|
|
98
|
+
{ type: 'earned', category: 'referral', referrer: not(origin), utmSource: none, utmMedium: none, tracking: none },
|
|
99
|
+
|
|
100
|
+
// OWNED
|
|
101
|
+
{ type: 'owned', category: 'direct', referrer: none, utmSource: none, utmMedium: none, tracking: none },
|
|
102
|
+
{ type: 'owned', category: 'internal', referrer: anyOf(origin), utmSource: none, utmMedium: none, tracking: none },
|
|
103
|
+
{ type: 'owned', category: 'email', referrer: any, utmSource: any, utmMedium: any, tracking: anyOf(emailTrackingParams) },
|
|
104
|
+
{ type: 'owned', category: 'email', referrer: any, utmSource: any, utmMedium: anyOf(emailUTMMediums), tracking: any },
|
|
105
|
+
{ type: 'owned', category: 'sms', referrer: none, utmSource: any, utmMedium: anyOf(smsUTMMediums), tracking: none },
|
|
106
|
+
{ type: 'owned', category: 'qr', referrer: none, utmSource: any, utmMedium: anyOf(qrUTMMediums), tracking: none },
|
|
107
|
+
{ type: 'owned', category: 'push', referrer: none, utmSource: any, utmMedium: anyOf(pushUTMMediums), tracking: none },
|
|
108
|
+
|
|
109
|
+
// FALLBACK
|
|
110
|
+
{ type: 'owned', category: 'uncategorized', referrer: any, utmSource: any, utmMedium: any, tracking: any },
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
export function classifyTrafficSource(url, referrer, utmSource, utmMedium, trackingParams) {
|
|
114
|
+
const { origin } = new URL(url);
|
|
115
|
+
const rules = RULES(origin);
|
|
116
|
+
|
|
117
|
+
const { type, category } = rules.find((rule) => (
|
|
118
|
+
rule.referrer(referrer)
|
|
119
|
+
&& rule.utmSource(utmSource)
|
|
120
|
+
&& rule.utmMedium(utmMedium)
|
|
121
|
+
&& rule.tracking(trackingParams)
|
|
122
|
+
));
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type,
|
|
126
|
+
category,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -47,12 +47,49 @@ function getOrCreateVariantObject(variants, variantName) {
|
|
|
47
47
|
return variantObject;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function updateInferredStartAndEndDate(experimentObject, time) {
|
|
51
|
+
const bundleTime = new Date(time);
|
|
52
|
+
const yesterday = new Date();
|
|
53
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
54
|
+
yesterday.setHours(0, 0, 0, 0);
|
|
55
|
+
const bundleDate = new Date(bundleTime);
|
|
56
|
+
bundleDate.setHours(0, 0, 0, 0);
|
|
57
|
+
if (!experimentObject.inferredStartDate && !experimentObject.inferredEndDate) {
|
|
58
|
+
// adding the inferredStartDate and inferredEndDate properties for the first time
|
|
59
|
+
// eslint-disable-next-line no-param-reassign
|
|
60
|
+
experimentObject.inferredStartDate = time;
|
|
61
|
+
// check if bundleTime is before yesterday
|
|
62
|
+
if (bundleDate < yesterday) {
|
|
63
|
+
// RUM data is delayed by a day, so if we don't have
|
|
64
|
+
// any RUM data for yesterday, so we can infer the endDate
|
|
65
|
+
// eslint-disable-next-line no-param-reassign
|
|
66
|
+
experimentObject.inferredEndDate = time;
|
|
67
|
+
} else {
|
|
68
|
+
// eslint-disable-next-line no-param-reassign
|
|
69
|
+
experimentObject.inferredEndDate = null;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
const inferredStartDateObj = new Date(experimentObject.inferredStartDate);
|
|
73
|
+
if (bundleTime < inferredStartDateObj) {
|
|
74
|
+
// eslint-disable-next-line no-param-reassign
|
|
75
|
+
experimentObject.inferredStartDate = time;
|
|
76
|
+
}
|
|
77
|
+
if (bundleDate < yesterday) {
|
|
78
|
+
if (!experimentObject.inferredEndDate
|
|
79
|
+
|| (bundleTime > new Date(experimentObject.inferredEndDate))) {
|
|
80
|
+
// eslint-disable-next-line no-param-reassign
|
|
81
|
+
experimentObject.inferredEndDate = time;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
function handler(bundles) {
|
|
51
88
|
const experimentInsights = {};
|
|
52
89
|
for (const bundle of bundles) {
|
|
53
|
-
const experimentEvent = bundle.events
|
|
90
|
+
const experimentEvent = bundle.events?.find((e) => e.checkpoint === 'experiment');
|
|
54
91
|
if (experimentEvent) {
|
|
55
|
-
const { url, weight } = bundle;
|
|
92
|
+
const { url, weight, time } = bundle;
|
|
56
93
|
if (!experimentInsights[url]) {
|
|
57
94
|
experimentInsights[url] = [];
|
|
58
95
|
}
|
|
@@ -60,6 +97,7 @@ function handler(bundles) {
|
|
|
60
97
|
const variantName = experimentEvent.target;
|
|
61
98
|
const experimentObject = getOrCreateExperimentObject(experimentInsights[url], experimentName);
|
|
62
99
|
const variantObject = getOrCreateVariantObject(experimentObject.variants, variantName);
|
|
100
|
+
updateInferredStartAndEndDate(experimentObject, time);
|
|
63
101
|
variantObject.views += weight;
|
|
64
102
|
|
|
65
103
|
const metrics = {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { classifyTrafficSource } from '../common/traffic.js';
|
|
14
|
+
|
|
15
|
+
function extractHints(bundle) {
|
|
16
|
+
const findEvent = (checkpoint, source = '') => bundle.events.find((e) => e.checkpoint === checkpoint && (!source || e.source === source)) || {};
|
|
17
|
+
|
|
18
|
+
const referrer = findEvent('enter').source || '';
|
|
19
|
+
const utmSource = findEvent('utm', 'utm_source').target || '';
|
|
20
|
+
const utmMedium = findEvent('utm', 'utm_medium').target || '';
|
|
21
|
+
const tracking = findEvent('paid').checkpoint || findEvent('email').checkpoint || '';
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
url: bundle.url,
|
|
25
|
+
weight: bundle.weight,
|
|
26
|
+
referrer,
|
|
27
|
+
utmSource,
|
|
28
|
+
utmMedium,
|
|
29
|
+
tracking,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectByUrlAndTrafficSource(acc, { url, weight, trafficSource }) {
|
|
34
|
+
acc[url] = acc[url] || { total: 0 };
|
|
35
|
+
acc[url][trafficSource] = (acc[url][trafficSource] || 0) + weight;
|
|
36
|
+
acc[url].total += weight;
|
|
37
|
+
return acc;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function transformFormat(trafficSources) {
|
|
41
|
+
return Object.entries(trafficSources).map(([url, value]) => ({
|
|
42
|
+
url,
|
|
43
|
+
total: value.total,
|
|
44
|
+
sources: Object.entries(value)
|
|
45
|
+
.filter(([source]) => source !== 'total')
|
|
46
|
+
.map(([source, views]) => ({ type: source, views })),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handler(bundles) {
|
|
51
|
+
const trafficSources = bundles
|
|
52
|
+
.map(extractHints)
|
|
53
|
+
.map((row) => {
|
|
54
|
+
const {
|
|
55
|
+
type,
|
|
56
|
+
category,
|
|
57
|
+
} = classifyTrafficSource(row.url, row.referrer, row.utmSource, row.utmMedium, row.tracking);
|
|
58
|
+
return {
|
|
59
|
+
url: row.url,
|
|
60
|
+
weight: row.weight,
|
|
61
|
+
trafficSource: `${type}:${category}`,
|
|
62
|
+
};
|
|
63
|
+
})
|
|
64
|
+
.reduce(collectByUrlAndTrafficSource, {});
|
|
65
|
+
|
|
66
|
+
return transformFormat(trafficSources)
|
|
67
|
+
.sort((a, b) => b.total - a.total); // sort desc by total views
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
handler,
|
|
72
|
+
checkpoints: ['email', 'enter', 'paid', 'utm'],
|
|
73
|
+
};
|
package/src/index.js
CHANGED
|
@@ -13,12 +13,14 @@ import { fetchBundles } from './common/rum-bundler-client.js';
|
|
|
13
13
|
import notfound from './functions/404.js';
|
|
14
14
|
import cwv from './functions/cwv.js';
|
|
15
15
|
import experiment from './functions/experiment.js';
|
|
16
|
+
import trafficAcquisition from './functions/traffic-acquisition.js';
|
|
16
17
|
import variant from './functions/variant.js';
|
|
17
18
|
|
|
18
19
|
const HANDLERS = {
|
|
19
20
|
404: notfound,
|
|
20
21
|
cwv,
|
|
21
22
|
experiment,
|
|
23
|
+
'traffic-acquisition': trafficAcquisition,
|
|
22
24
|
variant,
|
|
23
25
|
};
|
|
24
26
|
|