@adobe/spacecat-shared-rum-api-client 2.11.0 → 2.12.1

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.12.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.12.0...@adobe/spacecat-shared-rum-api-client-v2.12.1) (2024-11-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update external fixes ([#454](https://github.com/adobe/spacecat-shared/issues/454)) ([325cf8d](https://github.com/adobe/spacecat-shared/commit/325cf8dded5fcabadaf7d8fdd510d33aeafd08a7))
7
+
8
+ # [@adobe/spacecat-shared-rum-api-client-v2.12.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.11.0...@adobe/spacecat-shared-rum-api-client-v2.12.0) (2024-11-21)
9
+
10
+
11
+ ### Features
12
+
13
+ * **data-cruncher:** port queries to the data cruncher from RUM Explorer ([#375](https://github.com/adobe/spacecat-shared/issues/375)) ([599ebfc](https://github.com/adobe/spacecat-shared/commit/599ebfc7a7e14580ee1af9e42eac0d208acd5d94))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.11.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.10.0...@adobe/spacecat-shared-rum-api-client-v2.11.0) (2024-11-20)
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.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -39,8 +39,8 @@
39
39
  "@adobe/helix-shared-wrap": "2.0.2",
40
40
  "@adobe/helix-universal": "5.0.6",
41
41
  "@adobe/spacecat-shared-utils": "1.22.4",
42
+ "@adobe/rum-distiller": "1.9.0",
42
43
  "aws4": "1.13.2",
43
- "d3-array": "3.2.4",
44
44
  "urijs": "^1.19.11"
45
45
  },
46
46
  "devDependencies": {
@@ -49,6 +49,6 @@
49
49
  "nock": "13.5.6",
50
50
  "sinon": "19.0.2",
51
51
  "sinon-chai": "4.0.0",
52
- "typescript": "5.6.3"
52
+ "typescript": "5.7.2"
53
53
  }
54
54
  }
@@ -12,48 +12,6 @@
12
12
 
13
13
  import { extractTrafficHints, classifyVendor, getSecondLevelDomain } from './traffic.js';
14
14
 
15
- /**
16
- * Calculates the total page views by URL from an array of bundles.
17
- * @param {Array<Object>} bundles - An array of RUM bundles (NOT Flat bundles).
18
- * @returns {Object} An object where keys are URLs and values are the total page views for each URL.
19
- */
20
- function pageviewsByUrl(bundles) {
21
- return bundles.reduce((acc, cur) => {
22
- if (!acc[cur.url]) acc[cur.url] = 0;
23
- acc[cur.url] += cur.weight;
24
- return acc;
25
- }, {});
26
- }
27
-
28
- /**
29
- * Calculates the Click-Through Rate (CTR) by URL.
30
- * CTR is defined as the total number of sessions with at least one click event
31
- * divided by the total number of pageviews for each URL.
32
- *
33
- * @param {Array<Object>} bundles - An array of RUM bundles (NOT Flat bundles).
34
- * @returns {Object} - An object where the key is the URL and the value is the CTR value.
35
- */
36
- function getCTRByUrl(bundles) {
37
- const aggregated = bundles.reduce((acc, bundle) => {
38
- const { url } = bundle;
39
- if (!acc[url]) {
40
- acc[url] = { sessionsWithClick: 0, totalPageviews: 0 };
41
- }
42
- const hasClick = bundle.events.some((event) => event.checkpoint === 'click');
43
-
44
- acc[url].totalPageviews += bundle.weight;
45
- if (hasClick) {
46
- acc[url].sessionsWithClick += bundle.weight;
47
- }
48
- return acc;
49
- }, {});
50
- return Object.entries(aggregated)
51
- .reduce((acc, [url, { sessionsWithClick, totalPageviews }]) => {
52
- acc[url] = (sessionsWithClick / totalPageviews);
53
- return acc;
54
- }, {});
55
- }
56
-
57
15
  /**
58
16
  * Calculates the Click-Through Rate (CTR) by URL and Referrer.
59
17
  * CTR is defined as the total number of sessions with at least one click event per referrer.
@@ -132,7 +90,5 @@ function getSiteAvgCTR(bundles) {
132
90
 
133
91
  export {
134
92
  getSiteAvgCTR,
135
- getCTRByUrl,
136
93
  getCTRByUrlAndVendor,
137
- pageviewsByUrl,
138
94
  };
@@ -122,6 +122,7 @@ const anyOf = (truth) => (text) => {
122
122
  return truth === text;
123
123
  };
124
124
 
125
+ /* c8 ignore next 1 */
125
126
  const none = (input) => (Array.isArray(input) ? input.length === 0 : !hasText(input));
126
127
 
127
128
  const not = (truth) => (text) => {
@@ -198,6 +199,7 @@ const RULES = (domain) => ([
198
199
  ]);
199
200
 
200
201
  export function extractTrafficHints(bundle) {
202
+ /* c8 ignore next 1 */
201
203
  const findEvent = (checkpoint, source = '') => bundle.events.find((e) => e.checkpoint === checkpoint && (!source || e.source === source)) || {};
202
204
 
203
205
  const referrer = findEvent('enter').source || '';
@@ -252,3 +254,20 @@ export function classifyTrafficSource(url, referrer, utmSource, utmMedium, track
252
254
  vendor,
253
255
  };
254
256
  }
257
+
258
+ export function classifyTraffic(bundle) {
259
+ const {
260
+ url,
261
+ weight,
262
+ referrer,
263
+ utmSource,
264
+ utmMedium,
265
+ tracking,
266
+ } = extractTrafficHints(bundle);
267
+
268
+ return {
269
+ url,
270
+ weight,
271
+ ...classifyTrafficSource(url, referrer, utmSource, utmMedium, tracking),
272
+ };
273
+ }
@@ -10,62 +10,36 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { quantile } from 'd3-array';
14
- import { pageviewsByUrl } from '../common/aggregateFns.js';
15
- import { FlatBundle } from '../common/flat-bundle.js';
16
-
17
- const CWV_METRICS = ['lcp', 'cls', 'inp', 'ttfb'].map((metric) => `cwv-${metric}`);
18
-
19
- function collectCWVs(groupedByUrlIdTime) {
20
- const { url, items: itemsByUrl } = groupedByUrlIdTime;
21
-
22
- // first level: grouped by url
23
- const CWVs = itemsByUrl.reduce((acc, { items: itemsById }) => {
24
- // second level: grouped by id
25
- const itemsByTime = itemsById.flatMap((itemById) => itemById.items);
26
- // third level: grouped by time
27
- const maximums = itemsByTime.reduce((values, item) => {
28
- // each session (id-time) can contain multiple measurement for the same metric
29
- // we need to find the maximum per metric type
30
- // eslint-disable-next-line no-param-reassign
31
- values[item.checkpoint] = Math.max(values[item.checkpoint] || 0, item.value);
32
- return values;
33
- }, {});
34
-
35
- // max values per id for each metric type are collected into an array
36
- CWV_METRICS.forEach((metric) => {
37
- if (!acc[metric]) acc[metric] = [];
38
- if (maximums[metric]) {
39
- acc[metric].push(maximums[metric]);
40
- }
41
- });
42
- return acc;
43
- }, {});
44
-
45
- return {
46
- url,
47
- lcp: quantile(CWVs['cwv-lcp'], 0.75) || null,
48
- lcpCount: CWVs['cwv-lcp'].length,
49
- cls: quantile(CWVs['cwv-cls'], 0.75) || null,
50
- clsCount: CWVs['cwv-cls'].length,
51
- inp: quantile(CWVs['cwv-inp'], 0.75) || null,
52
- inpCount: CWVs['cwv-inp'].length,
53
- ttfb: quantile(CWVs['cwv-ttfb'], 0.75) || null,
54
- ttfbCount: CWVs['cwv-ttfb'].length,
55
- };
56
- }
13
+ import {
14
+ DataChunks, series, facets,
15
+ } from '@adobe/rum-distiller';
16
+ import { loadBundles } from '../utils.js';
57
17
 
58
18
  function handler(bundles) {
59
- const pageviews = pageviewsByUrl(bundles);
60
-
61
- return FlatBundle.fromArray(bundles)
62
- .groupBy('url', 'id', 'time')
63
- .map(collectCWVs)
19
+ const dataChunks = new DataChunks();
20
+
21
+ loadBundles(bundles, dataChunks);
22
+
23
+ dataChunks.addFacet('urls', facets.url);
24
+
25
+ dataChunks.addSeries('lcp', series.lcp);
26
+ dataChunks.addSeries('cls', series.cls);
27
+ dataChunks.addSeries('inp', series.inp);
28
+ dataChunks.addSeries('ttfb', series.ttfb);
29
+
30
+ return dataChunks.facets.urls.map((urlFacet) => ({
31
+ url: urlFacet.value,
32
+ pageviews: urlFacet.weight,
33
+ lcp: urlFacet.metrics.lcp.percentile(75) || null,
34
+ lcpCount: urlFacet.metrics.lcp.count,
35
+ cls: urlFacet.metrics.cls.percentile(75) || null,
36
+ clsCount: urlFacet.metrics.cls.count,
37
+ inp: urlFacet.metrics.inp.percentile(75) || null,
38
+ inpCount: urlFacet.metrics.inp.count,
39
+ ttfb: urlFacet.metrics.ttfb.percentile(75) || null,
40
+ ttfbCount: urlFacet.metrics.ttfb.count,
41
+ }))
64
42
  .filter((row) => row.lcp || row.cls || row.inp || row.ttfb) // filter out pages with no cwv data
65
- .map((acc) => {
66
- acc.pageviews = pageviews[acc.url];
67
- return acc;
68
- })
69
43
  .sort((a, b) => b.pageviews - a.pageviews); // sort desc by pageviews
70
44
  }
71
45
 
@@ -10,159 +10,82 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- const EXPERIMENT_CHECKPOINT = ['experiment'];
14
- const METRIC_CHECKPOINTS = ['click', 'convert', 'formsubmit'];
15
- const CHECKPOINTS = [...EXPERIMENT_CHECKPOINT, ...METRIC_CHECKPOINTS];
13
+ import { DataChunks } from '@adobe/rum-distiller';
14
+ import { generateKey, DELIMITER, loadBundles } from '../utils.js';
16
15
 
17
- function toClassName(name) {
18
- return typeof name === 'string'
19
- ? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
20
- : '';
21
- }
16
+ const METRICS = ['click', 'convert', 'formsubmit'];
22
17
 
23
- function getOrCreateExperimentObject(urlInsights, experimentName) {
24
- let experimentObject = urlInsights.find((e) => e.experiment === toClassName(experimentName));
25
- if (!experimentObject) {
26
- experimentObject = {
27
- experiment: toClassName(experimentName),
28
- variants: [],
29
- };
30
- urlInsights.push(experimentObject);
31
- }
32
- return experimentObject;
33
- }
18
+ const experimentsFacetFn = (bundle) => bundle.events.filter((e) => e.checkpoint === 'experiment').map((e) => generateKey(bundle.url, e.source));
19
+ const variantsFacetFn = (bundle) => bundle.events.filter((e) => e.checkpoint === 'experiment').map((e) => generateKey(bundle.url, e.source, e.target));
34
20
 
35
- function getOrCreateVariantObject(variants, variantName) {
36
- let variantObject = variants.find((v) => v.name === variantName);
37
- if (!variantObject) {
38
- variantObject = {
39
- name: variantName,
40
- views: 0,
41
- samples: 0,
42
- click: {},
43
- convert: {},
44
- formsubmit: {},
45
- };
46
- variants.push(variantObject);
47
- }
48
- return variantObject;
49
- }
21
+ const checkpointsFacetFn = (bundle) => {
22
+ const experiments = bundle.events.filter((e) => e.checkpoint === 'experiment');
23
+ const metrics = bundle.events.filter((c) => METRICS.includes(c.checkpoint) && c.source);
50
24
 
51
- function updateInferredStartAndEndDate(experimentObject, time) {
52
- const bundleTime = new Date(time);
53
- const yesterday = new Date();
54
- yesterday.setDate(yesterday.getDate() - 1);
55
- yesterday.setHours(0, 0, 0, 0);
56
- const bundleDate = new Date(bundleTime);
57
- bundleDate.setHours(0, 0, 0, 0);
58
- if (!experimentObject.inferredStartDate && !experimentObject.inferredEndDate) {
59
- // eslint-disable-next-line no-param-reassign
60
- experimentObject.inferredStartDate = time;
61
- // eslint-disable-next-line no-param-reassign
62
- experimentObject.inferredEndDate = time;
63
- } else {
64
- const inferredStartDateObj = new Date(experimentObject.inferredStartDate);
65
- const inferredEndDateObj = new Date(experimentObject.inferredEndDate);
66
- if (bundleTime < inferredStartDateObj) {
67
- // eslint-disable-next-line no-param-reassign
68
- experimentObject.inferredStartDate = time;
69
- }
70
- if (bundleTime > inferredEndDateObj) {
71
- // eslint-disable-next-line no-param-reassign
72
- experimentObject.inferredEndDate = time;
73
- }
74
- }
75
- }
25
+ const keys = experiments.flatMap(
26
+ (exp) => metrics.flatMap((metric) => [
27
+ generateKey(bundle.url, exp.source, exp.target, metric.checkpoint, metric.source),
28
+ generateKey(bundle.url, exp.source, exp.target, metric.checkpoint, '*'),
29
+ ]),
30
+ );
76
31
 
77
- function calculateMetrics(bundle) {
78
- const metrics = {};
79
- for (const checkpoint of METRIC_CHECKPOINTS) {
80
- metrics[checkpoint] = {};
81
- }
82
- for (const event of bundle.events) {
83
- if (METRIC_CHECKPOINTS.includes(event.checkpoint)) {
84
- const { source, checkpoint } = event;
85
- if (!metrics[checkpoint][source]) {
86
- metrics[checkpoint][source] = {
87
- value: bundle.weight,
88
- samples: 1,
89
- };
90
- } else {
91
- metrics[checkpoint][source].value += bundle.weight;
92
- metrics[checkpoint][source].samples += 1;
93
- }
94
- }
95
- }
96
- return metrics;
97
- }
32
+ return [...new Set(keys)];
33
+ };
98
34
 
99
35
  function handler(bundles) {
100
- const experimentInsights = {};
101
- for (const bundle of bundles) {
102
- const experimentEvents = bundle.events?.filter(
103
- (e) => (EXPERIMENT_CHECKPOINT.includes(e.checkpoint) && e.source),
104
- );
105
- const { url, weight, time } = bundle;
106
- const metrics = calculateMetrics(bundle);
107
- for (const experimentEvent of experimentEvents) {
108
- if (!experimentInsights[url]) {
109
- experimentInsights[url] = [];
110
- }
111
- const experimentName = experimentEvent.source;
112
- const variantName = experimentEvent.target;
113
- const experimentObject = getOrCreateExperimentObject(
114
- experimentInsights[url],
115
- experimentName,
116
- );
117
- const variantObject = getOrCreateVariantObject(experimentObject.variants, variantName);
118
- updateInferredStartAndEndDate(experimentObject, time);
119
- variantObject.views += weight;
120
- variantObject.samples += 1;
121
- // combine metrics and variantObject, considering the interaction events
122
- // only once during the session
123
- for (const checkpoint of METRIC_CHECKPOINTS) {
124
- // eslint-disable-next-line no-restricted-syntax
125
- for (const source in metrics?.[checkpoint]) {
126
- if (!variantObject[checkpoint][source]) {
127
- variantObject[checkpoint][source] = {
128
- value: weight,
129
- samples: 1,
130
- };
131
- } else {
132
- variantObject[checkpoint][source].value += weight;
133
- variantObject[checkpoint][source].samples += 1;
134
- }
135
- }
136
- }
137
- // add each metric to the variantObject's * count by weight
138
- for (const checkpoint of Object.keys(metrics)) {
139
- if (Object.keys(metrics[checkpoint]).length > 0) {
140
- if (!variantObject[checkpoint]['*']) {
141
- variantObject[checkpoint]['*'] = {
142
- value: weight,
143
- samples: 1,
144
- };
145
- } else {
146
- variantObject[checkpoint]['*'].value += weight;
147
- variantObject[checkpoint]['*'].samples += 1;
148
- }
149
- }
150
- }
151
- // add global interactionsCount if there's any interaction
152
- const hasInteraction = Object.values(metrics).some((m) => Object.keys(m).length > 0);
153
- if (hasInteraction) {
154
- if (!variantObject.interactionsCount) {
155
- variantObject.interactionsCount = weight;
156
- } else {
157
- variantObject.interactionsCount += weight;
158
- }
159
- }
160
- }
161
- }
162
- return experimentInsights;
36
+ const dataChunks = new DataChunks();
37
+
38
+ loadBundles(bundles.filter((bundle) => bundle.events.some((event) => event.checkpoint === 'experiment')), dataChunks);
39
+
40
+ dataChunks.addFacet('experiments', experimentsFacetFn);
41
+ dataChunks.addFacet('variants', variantsFacetFn);
42
+ dataChunks.addFacet('checkpoints', checkpointsFacetFn);
43
+
44
+ dataChunks.addSeries('experimenttime', (bundle) => new Date(bundle.time).getTime());
45
+ dataChunks.addSeries('views', (bundle) => bundle.weight);
46
+ dataChunks.addSeries('interaction', (bundle) => (bundle.events.find((e) => e.checkpoint === 'click' || e.checkpoint === 'formsubmit' || e.checkpoint === 'convert') ? bundle.weight : undefined));
47
+
48
+ const { experiments, variants, checkpoints } = dataChunks.facets;
49
+
50
+ const result = {};
51
+ experiments.forEach((uev) => {
52
+ const [url, experiment] = uev.value.split(DELIMITER);
53
+ if (!result[url]) result[url] = [];
54
+ result[url].push({
55
+ experiment,
56
+ variants: [],
57
+ inferredStartDate: new Date(uev.metrics.experimenttime.min).toISOString(),
58
+ inferredEndDate: new Date(uev.metrics.experimenttime.max).toISOString(),
59
+ });
60
+ });
61
+ variants.forEach((uev) => {
62
+ const [url, experiment, variant] = uev.value.split(DELIMITER);
63
+ const eIdx = result[url].findIndex((e) => e.experiment === experiment);
64
+ result[url][eIdx].variants.push({
65
+ name: variant,
66
+ click: {},
67
+ formsubmit: {},
68
+ convert: {},
69
+ interactionsCount: uev.metrics.interaction.sum,
70
+ samples: uev.metrics.views.count,
71
+ views: uev.metrics.views.sum,
72
+ });
73
+ });
74
+ checkpoints.forEach((uev) => {
75
+ const [url, experiment, variant, checkpoint, source] = uev.value.split(DELIMITER);
76
+ const eIdx = result[url].findIndex((e) => e.experiment === experiment);
77
+ const vIdx = result[url][eIdx].variants.findIndex((v) => v.name === variant);
78
+
79
+ result[url][eIdx].variants[vIdx][checkpoint][source] = {
80
+ value: uev.metrics.views.sum,
81
+ samples: uev.metrics.views.count,
82
+ };
83
+ });
84
+
85
+ return result;
163
86
  }
164
87
 
165
88
  export default {
166
89
  handler,
167
- checkpoints: CHECKPOINTS,
90
+ checkpoints: [...METRICS, 'experiment'],
168
91
  };
@@ -10,8 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import trafficAcquisition from '../traffic-acquisition.js';
14
- import { getCTRByUrl } from '../../common/aggregateFns.js';
13
+ import { DataChunks, facets } from '@adobe/rum-distiller';
14
+ import { loadBundles, trafficSeriesFn } from '../../utils.js';
15
15
 
16
16
  const HOMEPAGE_PAID_TRAFFIC_THRESHOLD = 0.8;
17
17
  const NON_HOMEPAGE_PAID_TRAFFIC_THRESHOLD = 0.5;
@@ -44,8 +44,7 @@ function convertToOpportunity(traffic) {
44
44
  };
45
45
  }
46
46
 
47
- function hasHighInorganicTraffic(traffic) {
48
- const { url, paid, total } = traffic;
47
+ function hasHighInorganicTraffic(url, paid, total) {
49
48
  const isHomepage = new URL(url).pathname === '/';
50
49
  const threshold = isHomepage
51
50
  ? HOMEPAGE_PAID_TRAFFIC_THRESHOLD
@@ -53,19 +52,39 @@ function hasHighInorganicTraffic(traffic) {
53
52
  return paid / total > threshold;
54
53
  }
55
54
 
56
- function hasHighBounceRate(ctr) {
57
- return ctr < BOUNCE_RATE_THRESHOLD;
58
- }
59
-
60
55
  function handler(bundles, opts = {}) {
61
56
  const { interval = 7 } = opts;
62
- const trafficByUrl = trafficAcquisition.handler(bundles);
63
- const ctrByUrl = getCTRByUrl(bundles);
64
57
 
65
- return trafficByUrl.filter((traffic) => traffic.total > interval * DAILY_PAGEVIEW_THRESHOLD)
66
- .filter(hasHighInorganicTraffic)
67
- .filter((traffic) => hasHighBounceRate(ctrByUrl[traffic.url]))
68
- .map((traffic) => ({ ...traffic, bounceRate: 1 - ctrByUrl[traffic.url] }))
58
+ const dataChunks = new DataChunks();
59
+
60
+ loadBundles(bundles, dataChunks);
61
+
62
+ dataChunks.addFacet('urls', facets.url);
63
+
64
+ dataChunks.addSeries('views', (bundle) => bundle.weight);
65
+ dataChunks.addSeries('clicks', (bundle) => (bundle.events.some((e) => e.checkpoint === 'click') ? bundle.weight : 0));
66
+
67
+ const memo = {};
68
+ dataChunks.addSeries('earned', trafficSeriesFn(memo, 'earned'));
69
+ dataChunks.addSeries('owned', trafficSeriesFn(memo, 'owned'));
70
+ dataChunks.addSeries('paid', trafficSeriesFn(memo, 'paid'));
71
+
72
+ return dataChunks.facets.urls
73
+ .filter((url) => url.metrics.views.sum > interval * DAILY_PAGEVIEW_THRESHOLD)
74
+ .filter((url) => hasHighInorganicTraffic(
75
+ url.value,
76
+ url.metrics.paid.sum,
77
+ url.metrics.views.sum,
78
+ ))
79
+ .filter((url) => url.metrics.clicks.sum / url.metrics.views.sum < BOUNCE_RATE_THRESHOLD)
80
+ .map((url) => ({
81
+ url: url.value,
82
+ total: url.metrics.views.sum,
83
+ earned: url.metrics.earned.sum,
84
+ owned: url.metrics.owned.sum,
85
+ paid: url.metrics.paid.sum,
86
+ bounceRate: 1 - url.metrics.clicks.sum / url.metrics.views.sum,
87
+ }))
69
88
  .map(convertToOpportunity);
70
89
  }
71
90
 
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { classifyTrafficSource, extractTrafficHints } from '../common/traffic.js';
13
+ import { classifyTraffic } from '../common/traffic.js';
14
14
 
15
15
  const MAIN_TYPES = ['total', 'paid', 'earned', 'owned'];
16
16
 
@@ -40,21 +40,21 @@ function transformFormat(trafficSources) {
40
40
  }));
41
41
  }
42
42
 
43
+ function formatTraffic(row) {
44
+ const {
45
+ url, weight, type, category, vendor,
46
+ } = row;
47
+ return {
48
+ url,
49
+ weight,
50
+ trafficSource: vendor ? `${type}:${category}:${vendor}` : `${type}:${category}`,
51
+ };
52
+ }
53
+
43
54
  function handler(bundles) {
44
55
  const trafficSources = bundles
45
- .map(extractTrafficHints)
46
- .map((row) => {
47
- const {
48
- type,
49
- category,
50
- vendor,
51
- } = classifyTrafficSource(row.url, row.referrer, row.utmSource, row.utmMedium, row.tracking);
52
- return {
53
- url: row.url,
54
- weight: row.weight,
55
- trafficSource: vendor ? `${type}:${category}:${vendor}` : `${type}:${category}`,
56
- };
57
- })
56
+ .map(classifyTraffic)
57
+ .map(formatTraffic)
58
58
  .reduce(collectByUrlAndTrafficSource, {});
59
59
 
60
60
  return transformFormat(trafficSources)
package/src/utils.js CHANGED
@@ -11,6 +11,26 @@
11
11
  */
12
12
 
13
13
  import { context as h2, h1 } from '@adobe/fetch';
14
+ import { utils } from '@adobe/rum-distiller';
15
+ import { classifyTraffic } from './common/traffic.js';
16
+
17
+ export const DELIMITER = '≡';
18
+
19
+ export const generateKey = (...keys) => keys.join(DELIMITER);
20
+
21
+ export const trafficSeriesFn = (memo, type) => (bundle) => {
22
+ const key = generateKey(bundle.url, bundle.id, bundle.time);
23
+ if (!memo[key]) {
24
+ // eslint-disable-next-line no-param-reassign
25
+ memo[key] = classifyTraffic(bundle).type;
26
+ }
27
+
28
+ return type === memo[key] ? bundle.weight : 0;
29
+ };
30
+
31
+ export const loadBundles = (bundles, dataChunks) => {
32
+ dataChunks.load([{ rumBundles: bundles.map(utils.addCalculatedProps) }]);
33
+ };
14
34
 
15
35
  /* c8 ignore next 3 */
16
36
  export const { fetch } = process.env.HELIX_FETCH_FORCE_HTTP1