@adobe/spacecat-shared-rum-api-client 2.2.1 → 2.4.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [@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)
2
+
3
+
4
+ ### Features
5
+
6
+ * traffic acquisition detection ([#286](https://github.com/adobe/spacecat-shared/issues/286)) ([b3d1f1c](https://github.com/adobe/spacecat-shared/commit/b3d1f1caa288ed5cdda367b64dea24886cf87afb))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Features
12
+
13
+ * audit top pages for language mismatch (SITES-22690) ([#289](https://github.com/adobe/spacecat-shared/issues/289)) ([ec9d83a](https://github.com/adobe/spacecat-shared/commit/ec9d83a9f59ba7c88b2be3181c60290eae132219))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.2.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.2.0...@adobe/spacecat-shared-rum-api-client-v2.2.1) (2024-07-08)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-rum-api-client",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
+ }
@@ -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
+ };
@@ -0,0 +1,121 @@
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 { isObject } from '@adobe/spacecat-shared-utils';
14
+
15
+ const VARIANT_CHECKPOINT = 'variant';
16
+
17
+ function getOrCreateLanguageObject(languageInsights, language) {
18
+ let languageObject = languageInsights.find((l) => l.language === language);
19
+
20
+ if (isObject(languageObject)) {
21
+ return languageObject;
22
+ }
23
+
24
+ languageObject = {
25
+ language,
26
+ count: 0,
27
+ mismatches: {
28
+ type1: { preferredLanguages: {} }, // Type 1 mismatches
29
+ type2: { preferredLanguages: {} }, // Type 2 mismatches
30
+ type3: { preferredLanguages: {} }, // Type 3 mismatches
31
+ },
32
+ regions: {}, // Tracks the count of events for each region
33
+ };
34
+
35
+ languageInsights.push(languageObject);
36
+ return languageObject;
37
+ }
38
+
39
+ function handler(bundles) {
40
+ const languageInsights = [];
41
+
42
+ for (const bundle of bundles) {
43
+ let preferredLanguages = [];
44
+ let pageLanguage = null;
45
+ let userRegion = null;
46
+
47
+ for (const event of bundle.events) {
48
+ if (event.checkpoint === VARIANT_CHECKPOINT) {
49
+ const { target, source } = event;
50
+
51
+ if (source === 'preferred-languages') {
52
+ preferredLanguages = target.split(',').map((lang) => lang.trim());
53
+ } else if (source === 'page-language') {
54
+ pageLanguage = target;
55
+ } else if (source === 'user-region') {
56
+ userRegion = target;
57
+ }
58
+ }
59
+ }
60
+
61
+ if (pageLanguage) {
62
+ const languageObject = getOrCreateLanguageObject(languageInsights, pageLanguage);
63
+ languageObject.count += 1; // Increment the total count for this page language
64
+
65
+ // Type 1 Mismatch: List out each mismatch if the preferred language list
66
+ // does not contain the page language
67
+ const isType1Mismatch = !preferredLanguages.includes(pageLanguage);
68
+ if (isType1Mismatch) {
69
+ preferredLanguages.forEach((preferredLanguage) => {
70
+ if (!languageObject.mismatches.type1.preferredLanguages[preferredLanguage]) {
71
+ languageObject.mismatches.type1.preferredLanguages[preferredLanguage] = 1;
72
+ } else {
73
+ languageObject.mismatches.type1.preferredLanguages[preferredLanguage] += 1;
74
+ }
75
+ });
76
+ }
77
+
78
+ // Type 2 Mismatch: Count as one mismatch if any preferred language
79
+ // is different from page language
80
+ const isType2Mismatch = preferredLanguages.some(
81
+ (preferredLanguage) => preferredLanguage !== pageLanguage,
82
+ );
83
+ if (isType2Mismatch) {
84
+ const preferredLanguage = preferredLanguages.join(',');
85
+ if (!languageObject.mismatches.type2.preferredLanguages[preferredLanguage]) {
86
+ languageObject.mismatches.type2.preferredLanguages[preferredLanguage] = 1;
87
+ } else {
88
+ languageObject.mismatches.type2.preferredLanguages[preferredLanguage] += 1;
89
+ }
90
+ }
91
+
92
+ // Type 3 Mismatch: Compare each language in preferred language list to page language,
93
+ // and count each mismatch
94
+ preferredLanguages.forEach((preferredLanguage) => {
95
+ if (preferredLanguage !== pageLanguage) {
96
+ if (!languageObject.mismatches.type3.preferredLanguages[preferredLanguage]) {
97
+ languageObject.mismatches.type3.preferredLanguages[preferredLanguage] = 1;
98
+ } else {
99
+ languageObject.mismatches.type3.preferredLanguages[preferredLanguage] += 1;
100
+ }
101
+ }
102
+ });
103
+
104
+ // Track regions
105
+ if (userRegion) {
106
+ if (!languageObject.regions[userRegion]) {
107
+ languageObject.regions[userRegion] = 1;
108
+ } else {
109
+ languageObject.regions[userRegion] += 1;
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ return languageInsights;
116
+ }
117
+
118
+ export default {
119
+ handler,
120
+ checkpoints: VARIANT_CHECKPOINT,
121
+ };
package/src/index.js CHANGED
@@ -13,11 +13,15 @@ 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';
17
+ import variant from './functions/variant.js';
16
18
 
17
19
  const HANDLERS = {
18
20
  404: notfound,
19
21
  cwv,
20
22
  experiment,
23
+ 'traffic-acquisition': trafficAcquisition,
24
+ variant,
21
25
  };
22
26
 
23
27
  export default class RUMAPIClient {