@adobe/spacecat-shared-rum-api-client 2.42.0 → 2.43.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,9 @@
1
+ ## [@adobe/spacecat-shared-rum-api-client-v2.43.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.42.0...@adobe/spacecat-shared-rum-api-client-v2.43.0) (2026-06-15)
2
+
3
+ ### Features
4
+
5
+ * **rum-api-client:** add resolveRumDomainKey shared utility ([#1662](https://github.com/adobe/spacecat-shared/issues/1662)) ([c3249f0](https://github.com/adobe/spacecat-shared/commit/c3249f011727e9297b191bf6ca0a78d6f462942e))
6
+
1
7
  ## [@adobe/spacecat-shared-rum-api-client-v2.42.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.41.1...@adobe/spacecat-shared-rum-api-client-v2.42.0) (2026-06-09)
2
8
 
3
9
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-rum-api-client",
3
- "version": "2.42.0",
3
+ "version": "2.43.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
package/src/client.js ADDED
@@ -0,0 +1,209 @@
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
+ import { hasText, fetch } from '@adobe/spacecat-shared-utils';
13
+ import { fetchBundles, createBundleStream } from './common/rum-bundler-client.js';
14
+ import notfound from './functions/404.js';
15
+ import notfoundInternalLinks from './functions/404-internal-links.js';
16
+ import cwv from './functions/cwv.js';
17
+ import formVitals from './functions/form-vitals.js';
18
+ import experiment from './functions/experiment.js';
19
+ import trafficAcquisition from './functions/traffic-acquisition.js';
20
+ import totalMetrics from './functions/total-metrics.js';
21
+ import variant from './functions/variant.js';
22
+ import pageviews from './functions/pageviews.js';
23
+ import trafficMetrics from './functions/traffic-metrics.js';
24
+ import rageclick from './functions/opportunities/rageclick.js';
25
+ import highInorganicHighBounceRate from './functions/opportunities/high-inorganic-high-bounce-rate.js';
26
+ import highOrganicLowCtr from './functions/opportunities/high-organic-low-ctr.js';
27
+ import trafficAnalysis from './functions/traffic-analysis.js';
28
+ import optimizationReportMetrics from './functions/reports/optimization/metrics.js';
29
+ import optimizationReportGraph from './functions/reports/optimization/graph.js';
30
+ import userEngagement from './functions/user-engagement.js';
31
+
32
+ // exported for tests
33
+ export const RUM_BUNDLER_API_HOST = 'https://bundles.aem.page';
34
+
35
+ const HANDLERS = {
36
+ 404: notfound,
37
+ '404-internal-links': notfoundInternalLinks,
38
+ cwv,
39
+ 'form-vitals': formVitals,
40
+ experiment,
41
+ 'traffic-acquisition': trafficAcquisition,
42
+ variant,
43
+ rageclick,
44
+ totalMetrics,
45
+ 'high-inorganic-high-bounce-rate': highInorganicHighBounceRate,
46
+ 'high-organic-low-ctr': highOrganicLowCtr,
47
+ pageviews,
48
+ trafficMetrics,
49
+ 'traffic-analysis': trafficAnalysis,
50
+ 'optimization-report-metrics': optimizationReportMetrics,
51
+ 'optimization-report-graph': optimizationReportGraph,
52
+ 'user-engagement': userEngagement,
53
+ };
54
+
55
+ function sanitize(opts) {
56
+ return {
57
+ ...opts,
58
+ /* c8 ignore next 1 */
59
+ ...(hasText(opts.domainkey) && { domainkey: `${opts.domainkey.slice(0, 3)}***` }),
60
+ };
61
+ }
62
+
63
+ export default class RUMAPIClient {
64
+ static createFrom(context) {
65
+ const { env, log = console } = context;
66
+ const { RUM_ADMIN_KEY: rumAdminKey } = env;
67
+
68
+ if (context.rumApiClient) {
69
+ return context.rumApiClient;
70
+ }
71
+
72
+ const client = new RUMAPIClient({ rumAdminKey }, log);
73
+ context.rumApiClient = client;
74
+ return client;
75
+ }
76
+
77
+ constructor({ rumAdminKey }, log) {
78
+ this.log = log;
79
+ this.rumAdminKey = rumAdminKey;
80
+ this.domainkeyCache = {};
81
+ }
82
+
83
+ async _exchangeDomainkey(domain) {
84
+ if (hasText(this.domainkeyCache[domain])) {
85
+ return this.domainkeyCache[domain];
86
+ }
87
+
88
+ const resp = await fetch(`${RUM_BUNDLER_API_HOST}/domainkey/${domain}`, {
89
+ headers: {
90
+ Authorization: `Bearer ${this.rumAdminKey}`,
91
+ },
92
+ });
93
+
94
+ if (!resp.ok) {
95
+ throw new Error(`Error during fetching domainkey for domain '${domain} using admin key. Status: ${resp.status}`);
96
+ }
97
+
98
+ try {
99
+ const json = await resp.json();
100
+ if (!hasText(json.domainkey)) {
101
+ throw new Error(`Unexpected response: ${JSON.stringify(json)}`);
102
+ }
103
+ this.domainkeyCache[domain] = json.domainkey;
104
+ return json.domainkey;
105
+ } catch (e) {
106
+ throw new Error(`Error during fetching domainkey for domain '${domain} using admin key. Error: ${e.message}`);
107
+ }
108
+ }
109
+
110
+ async _getDomainkey(opts) {
111
+ const { domain, domainkey } = opts;
112
+
113
+ if (!hasText(domainkey) && !hasText(this.rumAdminKey)) {
114
+ throw new Error('You need to provide a \'domainkey\' or set RUM_ADMIN_KEY env variable');
115
+ }
116
+
117
+ if (hasText(domainkey)) {
118
+ return domainkey;
119
+ }
120
+
121
+ return this._exchangeDomainkey(domain);
122
+ }
123
+
124
+ async retrieveDomainkey(domain) {
125
+ return this._exchangeDomainkey(domain);
126
+ }
127
+
128
+ // eslint-disable-next-line class-methods-use-this
129
+ async query(query, opts) {
130
+ const { handler, checkpoints } = HANDLERS[query] || {};
131
+ if (!handler) {
132
+ throw new Error(`Unknown query ${query}`);
133
+ }
134
+
135
+ try {
136
+ const domainkey = await this._getDomainkey(opts);
137
+ const bundles = await fetchBundles({
138
+ ...opts,
139
+ domainkey,
140
+ checkpoints,
141
+ }, this.log);
142
+
143
+ this.log.debug(`Query "${query}" fetched ${bundles.length} bundles`); // maybe even remove?
144
+ return handler(bundles, opts);
145
+ } catch (e) {
146
+ throw new Error(`Query '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
147
+ }
148
+ }
149
+
150
+ // eslint-disable-next-line class-methods-use-this
151
+ async queryMulti(queries, opts) {
152
+ const queryHandlers = [];
153
+ const allCheckpoints = new Set();
154
+
155
+ for (const query of queries) {
156
+ const { handler, checkpoints = [] } = HANDLERS[query] || {};
157
+
158
+ if (!handler) {
159
+ throw new Error(`Unknown query: ${query}`);
160
+ }
161
+
162
+ queryHandlers.push({ query, handler });
163
+ checkpoints.forEach((checkpoint) => allCheckpoints.add(checkpoint));
164
+ }
165
+
166
+ try {
167
+ const domainkey = await this._getDomainkey(opts);
168
+
169
+ // Fetch bundles with deduplicated checkpoints
170
+ const bundles = await fetchBundles({
171
+ ...opts,
172
+ domainkey,
173
+ checkpoints: [...allCheckpoints],
174
+ }, this.log);
175
+
176
+ const results = {};
177
+ this.log.debug(`Multi query ${JSON.stringify(queries.join(', '))} fetched ${bundles.length} bundles`); // maybe even remove?
178
+
179
+ // Execute each query handler sequentially
180
+ for (const { query, handler } of queryHandlers) {
181
+ // eslint-disable-next-line no-await-in-loop
182
+ results[query] = await handler(bundles, opts);
183
+ }
184
+
185
+ return results;
186
+ } catch (e) {
187
+ throw new Error(`Multi query failed. Queries: ${JSON.stringify(queries)}, Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
188
+ }
189
+ }
190
+
191
+ async queryStream(query, opts) {
192
+ const { handler, checkpoints } = HANDLERS[query] || {};
193
+ if (!handler) {
194
+ throw new Error(`Unknown query ${query}`);
195
+ }
196
+
197
+ try {
198
+ const domainkey = await this._getDomainkey(opts);
199
+ return createBundleStream({
200
+ ...opts,
201
+ domainkey,
202
+ checkpoints,
203
+ handler,
204
+ }, this.log);
205
+ } catch (e) {
206
+ throw new Error(`Query stream '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
207
+ }
208
+ }
209
+ }
package/src/index.d.ts CHANGED
@@ -50,6 +50,27 @@ export interface RUMAPIOptions {
50
50
  }>;
51
51
  }
52
52
 
53
+ /**
54
+ * Result returned by resolveRumDomainKey.
55
+ */
56
+ export interface RumDomainKeyResult {
57
+ /** True if a RUM domain key was found for any candidate domain. */
58
+ hasDomainKey: boolean;
59
+ /** True if the lookup was aborted due to the 3 s shared timeout. */
60
+ timedOut: boolean;
61
+ }
62
+
63
+ /**
64
+ * Checks whether a site has a RUM domain key by trying up to four candidate domains
65
+ * in priority order: overrideBaseURL, www.override, baseURL, www.base.
66
+ * Stops on the first successful lookup. The 3 s timeout is shared across all candidates.
67
+ *
68
+ * @param site - Site model instance (requires getId, getBaseURL, getConfig).
69
+ * @param context - Request/worker context (requires env and log).
70
+ * @returns { hasDomainKey, timedOut }
71
+ */
72
+ export function resolveRumDomainKey(site: object, context: UniversalContext): Promise<RumDomainKeyResult>;
73
+
53
74
  export default class RUMAPIClient {
54
75
  /**
55
76
  * Static factory method to create an instance of RUMAPIClient.
package/src/index.js CHANGED
@@ -9,201 +9,8 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import { hasText, fetch } from '@adobe/spacecat-shared-utils';
13
- import { fetchBundles, createBundleStream } from './common/rum-bundler-client.js';
14
- import notfound from './functions/404.js';
15
- import notfoundInternalLinks from './functions/404-internal-links.js';
16
- import cwv from './functions/cwv.js';
17
- import formVitals from './functions/form-vitals.js';
18
- import experiment from './functions/experiment.js';
19
- import trafficAcquisition from './functions/traffic-acquisition.js';
20
- import totalMetrics from './functions/total-metrics.js';
21
- import variant from './functions/variant.js';
22
- import pageviews from './functions/pageviews.js';
23
- import trafficMetrics from './functions/traffic-metrics.js';
24
- import rageclick from './functions/opportunities/rageclick.js';
25
- import highInorganicHighBounceRate from './functions/opportunities/high-inorganic-high-bounce-rate.js';
26
- import highOrganicLowCtr from './functions/opportunities/high-organic-low-ctr.js';
27
- import trafficAnalysis from './functions/traffic-analysis.js';
28
- import optimizationReportMetrics from './functions/reports/optimization/metrics.js';
29
- import optimizationReportGraph from './functions/reports/optimization/graph.js';
30
- import userEngagement from './functions/user-engagement.js';
12
+ import RUMAPIClient from './client.js';
31
13
 
32
- // exported for tests
33
- export const RUM_BUNDLER_API_HOST = 'https://bundles.aem.page';
34
-
35
- const HANDLERS = {
36
- 404: notfound,
37
- '404-internal-links': notfoundInternalLinks,
38
- cwv,
39
- 'form-vitals': formVitals,
40
- experiment,
41
- 'traffic-acquisition': trafficAcquisition,
42
- variant,
43
- rageclick,
44
- totalMetrics,
45
- 'high-inorganic-high-bounce-rate': highInorganicHighBounceRate,
46
- 'high-organic-low-ctr': highOrganicLowCtr,
47
- pageviews,
48
- trafficMetrics,
49
- 'traffic-analysis': trafficAnalysis,
50
- 'optimization-report-metrics': optimizationReportMetrics,
51
- 'optimization-report-graph': optimizationReportGraph,
52
- 'user-engagement': userEngagement,
53
- };
54
-
55
- function sanitize(opts) {
56
- return {
57
- ...opts,
58
- /* c8 ignore next 1 */
59
- ...(hasText(opts.domainkey) && { domainkey: `${opts.domainkey.slice(0, 3)}***` }),
60
- };
61
- }
62
-
63
- export default class RUMAPIClient {
64
- static createFrom(context) {
65
- const { env, log = console } = context;
66
- const { RUM_ADMIN_KEY: rumAdminKey } = env;
67
-
68
- if (context.rumApiClient) {
69
- return context.rumApiClient;
70
- }
71
-
72
- const client = new RUMAPIClient({ rumAdminKey }, log);
73
- context.rumApiClient = client;
74
- return client;
75
- }
76
-
77
- constructor({ rumAdminKey }, log) {
78
- this.log = log;
79
- this.rumAdminKey = rumAdminKey;
80
- this.domainkeyCache = {};
81
- }
82
-
83
- async _exchangeDomainkey(domain) {
84
- if (hasText(this.domainkeyCache[domain])) {
85
- return this.domainkeyCache[domain];
86
- }
87
-
88
- const resp = await fetch(`${RUM_BUNDLER_API_HOST}/domainkey/${domain}`, {
89
- headers: {
90
- Authorization: `Bearer ${this.rumAdminKey}`,
91
- },
92
- });
93
-
94
- if (!resp.ok) {
95
- throw new Error(`Error during fetching domainkey for domain '${domain} using admin key. Status: ${resp.status}`);
96
- }
97
-
98
- try {
99
- const json = await resp.json();
100
- if (!hasText(json.domainkey)) {
101
- throw new Error(`Unexpected response: ${JSON.stringify(json)}`);
102
- }
103
- this.domainkeyCache[domain] = json.domainkey;
104
- return json.domainkey;
105
- } catch (e) {
106
- throw new Error(`Error during fetching domainkey for domain '${domain} using admin key. Error: ${e.message}`);
107
- }
108
- }
109
-
110
- async _getDomainkey(opts) {
111
- const { domain, domainkey } = opts;
112
-
113
- if (!hasText(domainkey) && !hasText(this.rumAdminKey)) {
114
- throw new Error('You need to provide a \'domainkey\' or set RUM_ADMIN_KEY env variable');
115
- }
116
-
117
- if (hasText(domainkey)) {
118
- return domainkey;
119
- }
120
-
121
- return this._exchangeDomainkey(domain);
122
- }
123
-
124
- async retrieveDomainkey(domain) {
125
- return this._exchangeDomainkey(domain);
126
- }
127
-
128
- // eslint-disable-next-line class-methods-use-this
129
- async query(query, opts) {
130
- const { handler, checkpoints } = HANDLERS[query] || {};
131
- if (!handler) {
132
- throw new Error(`Unknown query ${query}`);
133
- }
134
-
135
- try {
136
- const domainkey = await this._getDomainkey(opts);
137
- const bundles = await fetchBundles({
138
- ...opts,
139
- domainkey,
140
- checkpoints,
141
- }, this.log);
142
-
143
- this.log.debug(`Query "${query}" fetched ${bundles.length} bundles`); // maybe even remove?
144
- return handler(bundles, opts);
145
- } catch (e) {
146
- throw new Error(`Query '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
147
- }
148
- }
149
-
150
- // eslint-disable-next-line class-methods-use-this
151
- async queryMulti(queries, opts) {
152
- const queryHandlers = [];
153
- const allCheckpoints = new Set();
154
-
155
- for (const query of queries) {
156
- const { handler, checkpoints = [] } = HANDLERS[query] || {};
157
-
158
- if (!handler) {
159
- throw new Error(`Unknown query: ${query}`);
160
- }
161
-
162
- queryHandlers.push({ query, handler });
163
- checkpoints.forEach((checkpoint) => allCheckpoints.add(checkpoint));
164
- }
165
-
166
- try {
167
- const domainkey = await this._getDomainkey(opts);
168
-
169
- // Fetch bundles with deduplicated checkpoints
170
- const bundles = await fetchBundles({
171
- ...opts,
172
- domainkey,
173
- checkpoints: [...allCheckpoints],
174
- }, this.log);
175
-
176
- const results = {};
177
- this.log.debug(`Multi query ${JSON.stringify(queries.join(', '))} fetched ${bundles.length} bundles`); // maybe even remove?
178
-
179
- // Execute each query handler sequentially
180
- for (const { query, handler } of queryHandlers) {
181
- // eslint-disable-next-line no-await-in-loop
182
- results[query] = await handler(bundles, opts);
183
- }
184
-
185
- return results;
186
- } catch (e) {
187
- throw new Error(`Multi query failed. Queries: ${JSON.stringify(queries)}, Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
188
- }
189
- }
190
-
191
- async queryStream(query, opts) {
192
- const { handler, checkpoints } = HANDLERS[query] || {};
193
- if (!handler) {
194
- throw new Error(`Unknown query ${query}`);
195
- }
196
-
197
- try {
198
- const domainkey = await this._getDomainkey(opts);
199
- return createBundleStream({
200
- ...opts,
201
- domainkey,
202
- checkpoints,
203
- handler,
204
- }, this.log);
205
- } catch (e) {
206
- throw new Error(`Query stream '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
207
- }
208
- }
209
- }
14
+ export default RUMAPIClient;
15
+ export { RUM_BUNDLER_API_HOST } from './client.js';
16
+ export { resolveRumDomainKey } from './rum-domain-key.js';
@@ -0,0 +1,111 @@
1
+ /*
2
+ * Copyright 2026 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 RUMAPIClient from './client.js';
14
+
15
+ const RUM_CHECK_TIMEOUT_MS = 3000;
16
+
17
+ /**
18
+ * Resolves whether a site has a RUM domain key by trying candidates in priority order:
19
+ * 1. overrideBaseURL hostname (from fetchConfig, when set)
20
+ * 2. www.{overrideHostname}
21
+ * 3. baseURL hostname
22
+ * 4. www.{baseHostname}
23
+ *
24
+ * Stops on the first successful lookup. The 3 s timeout is a shared budget across
25
+ * the full candidate loop, not a per-domain limit.
26
+ *
27
+ * @param {object} site - Site model instance (getBaseURL, getConfig, getId).
28
+ * @param {object} context - Request/worker context (env, log).
29
+ * @returns {Promise<{hasDomainKey: boolean, timedOut: boolean}>}
30
+ */
31
+ export async function resolveRumDomainKey(site, context) {
32
+ const { log } = context;
33
+ const siteId = site.getId();
34
+
35
+ const siteConfig = site.getConfig();
36
+ const overrideBaseURL = siteConfig.getFetchConfig()?.overrideBaseURL;
37
+
38
+ let overrideHostname = null;
39
+ if (overrideBaseURL) {
40
+ try {
41
+ overrideHostname = new URL(overrideBaseURL).hostname;
42
+ } catch {
43
+ log.warn(`[rum-domain-key] Malformed overrideBaseURL for site ${siteId}: ${overrideBaseURL}, falling back to baseURL`);
44
+ }
45
+ }
46
+
47
+ let baseHostname;
48
+ try {
49
+ baseHostname = new URL(site.getBaseURL()).hostname;
50
+ } catch {
51
+ log.error(`[rum-domain-key] Malformed baseURL for site ${siteId}: ${site.getBaseURL()}, skipping`);
52
+ return { hasDomainKey: false, timedOut: false };
53
+ }
54
+
55
+ const withWww = (d) => (d && !d.startsWith('www.') ? `www.${d}` : null);
56
+
57
+ const domains = [...new Set([
58
+ overrideHostname,
59
+ withWww(overrideHostname),
60
+ baseHostname,
61
+ withWww(baseHostname),
62
+ ].filter(Boolean))];
63
+
64
+ let hasDomainKey = false;
65
+ let timeoutId;
66
+ let timedOut = false;
67
+ let cancelled = false;
68
+
69
+ const rumApiClient = RUMAPIClient.createFrom(context);
70
+
71
+ try {
72
+ await Promise.race([
73
+ (async () => {
74
+ for (const domain of domains) {
75
+ if (cancelled) {
76
+ break;
77
+ }
78
+ try {
79
+ // eslint-disable-next-line no-await-in-loop
80
+ await rumApiClient.retrieveDomainkey(domain);
81
+ hasDomainKey = true;
82
+ return;
83
+ } catch (e) {
84
+ log.info(`[rum-domain-key] RUM check failed for ${domain}: ${e.message}`);
85
+ }
86
+ }
87
+ if (!hasDomainKey) {
88
+ log.warn(`[rum-domain-key] No domain key found for site ${siteId} across all candidates: ${domains.join(', ')}`);
89
+ }
90
+ })(),
91
+ new Promise((_, reject) => {
92
+ timeoutId = setTimeout(() => {
93
+ timedOut = true;
94
+ cancelled = true;
95
+ reject(new Error('RUM check timed out'));
96
+ }, RUM_CHECK_TIMEOUT_MS);
97
+ }),
98
+ ]);
99
+ } catch (e) {
100
+ if (timedOut) {
101
+ log.warn(`[rum-domain-key] RUM check timed out for site ${siteId} across: ${domains.join(', ')}`);
102
+ /* c8 ignore next 3 */
103
+ } else {
104
+ log.warn(`[rum-domain-key] Unexpected error during RUM check for site ${siteId}: ${e.message}`);
105
+ }
106
+ } finally {
107
+ clearTimeout(timeoutId);
108
+ }
109
+
110
+ return { hasDomainKey, timedOut };
111
+ }