@adobe/spacecat-shared-rum-api-client 2.6.0 → 2.7.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.7.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.7.0...@adobe/spacecat-shared-rum-api-client-v2.7.1) (2024-08-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * missing source in the experiment checkpoint ([#319](https://github.com/adobe/spacecat-shared/issues/319)) ([952a50a](https://github.com/adobe/spacecat-shared/commit/952a50a2d90bcbd6c4146460f0a51a178bf1e596))
7
+
8
+ # [@adobe/spacecat-shared-rum-api-client-v2.7.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.6.0...@adobe/spacecat-shared-rum-api-client-v2.7.0) (2024-08-06)
9
+
10
+
11
+ ### Features
12
+
13
+ * add support for multiple experiments on the same page ([#309](https://github.com/adobe/spacecat-shared/issues/309)) ([8252e1a](https://github.com/adobe/spacecat-shared/commit/8252e1a74885a73db6193d24facf439b571e9523))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.6.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.5.4...@adobe/spacecat-shared-rum-api-client-v2.6.0) (2024-08-02)
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.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -50,6 +50,109 @@ function getUrlChunks(urls, chunkSize) {
50
50
  .map((_, index) => urls.slice(index * chunkSize, (index + 1) * chunkSize));
51
51
  }
52
52
 
53
+ /* c8 ignore start */
54
+ /*
55
+ * throw-away code for a single customer who customized the experimentation engine
56
+ * this code will be removed once they start using the default exp engine
57
+ *
58
+ * this function fetches experiment manifests, then merges variants data into controls data
59
+ *
60
+ * ie:
61
+ *
62
+ * if the customer runs for an experiment where variants are as following:
63
+ * control: /
64
+ * challenger-1: /a1/
65
+ * challenger-2: /a2/
66
+ *
67
+ * then data for the `/a1/` and `/a2` are counted towards `/`'s data
68
+ */
69
+ async function mergeBundlesWithSameId(bundles) {
70
+ if (!bundles[0]?.url?.includes('bamboohr.com')) return bundles;
71
+ const prodBaseUrl = 'https://www.bamboohr.com/experiments/';
72
+ const previewBaseUrl = 'https://main--bamboohr-website--bamboohr.hlx.page/experiments/archive/';
73
+ const manifestUrls = [
74
+ ...new Set(bundles.flatMap((bundle) => bundle.events
75
+ .filter((e) => e.checkpoint === 'experiment')
76
+ .map((e) => e.source))),
77
+ ];
78
+ const manifestUrlPromises = manifestUrls.map(async (experiment) => {
79
+ try {
80
+ const response = await fetch(`${prodBaseUrl}${experiment}/manifest.json`);
81
+ if (!response.ok) {
82
+ throw new Error('manifest request failed');
83
+ }
84
+ const data = await response.json();
85
+ return { url: `${prodBaseUrl}${experiment}/manifest.json`, data };
86
+ } catch (error) {
87
+ try {
88
+ const previewUrlResponse = await fetch(`${previewBaseUrl}${experiment}/manifest.json`);
89
+ if (!previewUrlResponse.ok) {
90
+ throw new Error('manifest request failed');
91
+ }
92
+ const previewUrlData = await previewUrlResponse.json();
93
+ return { url: `${previewBaseUrl}${experiment}/manifest.json`, data: previewUrlData };
94
+ } catch (err) {
95
+ return { url: `${previewBaseUrl}${experiment}/manifest.json`, data: null };
96
+ }
97
+ }
98
+ });
99
+ const experiments = await Promise.all(manifestUrlPromises);
100
+ let hasSeenPages = false; // required for multi-page experiments
101
+ const variants = (await Promise.all(experiments.map((e) => e.data)))
102
+ .filter((json) => json && Object.keys(json).length > 0)
103
+ .flatMap((json) => json.experiences?.data ?? [])
104
+ .filter((data) => {
105
+ if (data.Name === 'Pages') {
106
+ hasSeenPages = true;
107
+ } else if (['Percentage Split', 'Label', 'Blocks'].includes(data.Name)) {
108
+ // reset the flag when we see the next experiment
109
+ hasSeenPages = false;
110
+ }
111
+ return data.Name === 'Pages' || (hasSeenPages && data.Name === '');
112
+ });
113
+
114
+ const mapping = variants.reduce((acc, cur) => {
115
+ Object.entries(cur)
116
+ .filter(([k]) => !['Name', 'Control'].includes(k))
117
+ .forEach(([, v]) => {
118
+ acc[new URL(v).pathname] = new URL(cur.Control).pathname;
119
+ });
120
+ return acc;
121
+ }, {});
122
+
123
+ const variantPaths = Object.keys(mapping);
124
+
125
+ const getControlPath = (url) => {
126
+ const path = new URL(url).pathname;
127
+ if (variantPaths.includes(path)) return mapping[path];
128
+ return path;
129
+ };
130
+
131
+ const byIdAndPath = bundles.reduce((acc, cur) => {
132
+ const controlPath = getControlPath(cur.url);
133
+ const key = `${cur.id}-${controlPath}`;
134
+ if (!acc[key]) acc[key] = [];
135
+ if (variantPaths.includes(new URL(cur.url).pathname)) {
136
+ // eslint-disable-next-line no-param-reassign
137
+ cur.url = new URL(controlPath, cur.url).href;
138
+ }
139
+ acc[key].push(cur);
140
+ return acc;
141
+ }, {});
142
+
143
+ const merged = Object.entries(byIdAndPath).flatMap(([, v]) => {
144
+ let value = v;
145
+ if (v.length > 1) {
146
+ v[0].events.push(...v.slice(1).flatMap((bundle) => bundle.events));
147
+ value = [v[0]];
148
+ }
149
+ return value;
150
+ });
151
+
152
+ return Object.values(merged);
153
+ }
154
+ /* c8 ignore end */
155
+
53
156
  async function fetchBundles(opts = {}) {
54
157
  const {
55
158
  domain,
@@ -84,7 +187,7 @@ async function fetchBundles(opts = {}) {
84
187
  const bundles = await Promise.all(responses.map((response) => response.json()));
85
188
  result.push(...bundles.flatMap((b) => b.rumBundles.map(filterBundles(checkpoints))));
86
189
  }
87
- return result;
190
+ return mergeBundlesWithSameId(result);
88
191
  }
89
192
 
90
193
  export {
@@ -73,41 +73,50 @@ function updateInferredStartAndEndDate(experimentObject, time) {
73
73
  }
74
74
  }
75
75
 
76
+ function calculateMetrics(bundle) {
77
+ const metrics = {};
78
+ for (const checkpoint of METRIC_CHECKPOINTS) {
79
+ metrics[checkpoint] = {};
80
+ }
81
+ for (const event of bundle.events) {
82
+ if (METRIC_CHECKPOINTS.includes(event.checkpoint)) {
83
+ const { source, checkpoint } = event;
84
+ if (!metrics[checkpoint][source]) {
85
+ metrics[checkpoint][source] = bundle.weight;
86
+ } else {
87
+ metrics[checkpoint][source] += bundle.weight;
88
+ }
89
+ }
90
+ }
91
+ return metrics;
92
+ }
93
+
76
94
  function handler(bundles) {
77
95
  const experimentInsights = {};
78
96
  for (const bundle of bundles) {
79
- const experimentEvent = bundle.events?.find((e) => e.checkpoint === 'experiment');
80
- if (experimentEvent) {
81
- const { url, weight, time } = bundle;
97
+ const experimentEvents = bundle.events?.filter(
98
+ (e) => (EXPERIMENT_CHECKPOINT.includes(e.checkpoint) && e.source),
99
+ );
100
+ const { url, weight, time } = bundle;
101
+ const metrics = calculateMetrics(bundle);
102
+ for (const experimentEvent of experimentEvents) {
82
103
  if (!experimentInsights[url]) {
83
104
  experimentInsights[url] = [];
84
105
  }
85
106
  const experimentName = experimentEvent.source;
86
107
  const variantName = experimentEvent.target;
87
- const experimentObject = getOrCreateExperimentObject(experimentInsights[url], experimentName);
108
+ const experimentObject = getOrCreateExperimentObject(
109
+ experimentInsights[url],
110
+ experimentName,
111
+ );
88
112
  const variantObject = getOrCreateVariantObject(experimentObject.variants, variantName);
89
113
  updateInferredStartAndEndDate(experimentObject, time);
90
114
  variantObject.views += weight;
91
-
92
- const metrics = {};
93
- for (const checkpoint of METRIC_CHECKPOINTS) {
94
- metrics[checkpoint] = {};
95
- }
96
- for (const event of bundle.events) {
97
- if (METRIC_CHECKPOINTS.includes(event.checkpoint)) {
98
- const { source, checkpoint } = event;
99
- if (!metrics[checkpoint][source]) {
100
- metrics[checkpoint][source] = weight;
101
- } else {
102
- metrics[checkpoint][source] += weight;
103
- }
104
- }
105
- }
106
115
  // combine metrics and variantObject, considering the interaction events
107
116
  // only once during the session
108
117
  for (const checkpoint of METRIC_CHECKPOINTS) {
109
118
  // eslint-disable-next-line no-restricted-syntax
110
- for (const source in metrics[checkpoint]) {
119
+ for (const source in metrics?.[checkpoint]) {
111
120
  if (!variantObject[checkpoint][source]) {
112
121
  variantObject[checkpoint][source] = weight;
113
122
  } else {
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  import { classifyTrafficSource } from '../common/traffic.js';
14
- import { fetch } from '../utils.js';
15
14
 
16
15
  function extractHints(bundle) {
17
16
  const findEvent = (checkpoint, source = '') => bundle.events.find((e) => e.checkpoint === checkpoint && (!source || e.source === source)) || {};
@@ -48,81 +47,8 @@ function transformFormat(trafficSources) {
48
47
  }));
49
48
  }
50
49
 
51
- /* c8 ignore start */
52
- /*
53
- * throw-away code for a single customer who customized the experimentation engine
54
- * this code will be removed once they start using the default exp engine
55
- *
56
- * this function fetches experiment manifests, then merges variants data into controls data
57
- *
58
- * ie:
59
- *
60
- * if the customer runs for an experiment where variants are as following:
61
- * control: /
62
- * challenger-1: /a1/
63
- * challenger-2: /a2/
64
- *
65
- * then data for the `/a1/` and `/a2` are counted towards `/`'s data
66
- */
67
- async function mergeBundlesWithSameId(bundles) {
68
- if (!bundles[0]?.url.includes('bamboohr.com')) return bundles;
69
- const manifestUrls = [
70
- ...new Set(bundles.flatMap((bundle) => bundle.events
71
- .filter((e) => e.checkpoint === 'experiment')
72
- .map((e) => e.source))),
73
- ].map((experiment) => fetch(`https://www.bamboohr.com/experiments/${experiment}/manifest.json`));
74
-
75
- const experiments = await Promise.all(manifestUrls);
76
- const variants = (await Promise.all(experiments.map((e) => e.json().catch(() => {}))))
77
- .filter((json) => json && Object.keys(json).length > 0)
78
- .flatMap((json) => json.experiences?.data ?? [])
79
- .filter((data) => data.Name === 'Pages');
80
-
81
- const mapping = variants.reduce((acc, cur) => {
82
- Object.entries(cur)
83
- .filter(([k]) => !['Name', 'Control'].includes(k))
84
- .forEach(([, v]) => {
85
- acc[new URL(v).pathname] = new URL(cur.Control).pathname;
86
- });
87
- return acc;
88
- }, {});
89
-
90
- const variantPaths = Object.keys(mapping);
91
-
92
- const getControlPath = (url) => {
93
- const path = new URL(url).pathname;
94
- if (variantPaths.includes(path)) return mapping[path];
95
- return path;
96
- };
97
-
98
- const byIdAndPath = bundles.reduce((acc, cur) => {
99
- const controlPath = getControlPath(cur.url);
100
- const key = `${cur.id}-${controlPath}`;
101
- if (!acc[key]) acc[key] = [];
102
- if (variantPaths.includes(new URL(cur.url).pathname)) {
103
- // eslint-disable-next-line no-param-reassign
104
- cur.url = new URL(controlPath, cur.url).href;
105
- }
106
- acc[key].push(cur);
107
- return acc;
108
- }, {});
109
-
110
- const merged = Object.entries(byIdAndPath).flatMap(([, v]) => {
111
- let value = v;
112
- if (v.length > 1) {
113
- v[0].events.push(...v.slice(1).flatMap((bundle) => bundle.events));
114
- value = [v[0]];
115
- }
116
- return value;
117
- });
118
-
119
- return Object.values(merged);
120
- }
121
- /* c8 ignore end */
122
-
123
50
  async function handler(bundles) {
124
- const merged = await mergeBundlesWithSameId(bundles);
125
- const trafficSources = merged
51
+ const trafficSources = bundles
126
52
  .map(extractHints)
127
53
  .map((row) => {
128
54
  const {