@adobe/spacecat-shared-rum-api-client 2.36.5 → 2.37.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,10 @@
1
+ # [@adobe/spacecat-shared-rum-api-client-v2.37.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.36.5...@adobe/spacecat-shared-rum-api-client-v2.37.0) (2025-08-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * added metrics and graph data functions for optimization-report ([#919](https://github.com/adobe/spacecat-shared/issues/919)) ([29461b2](https://github.com/adobe/spacecat-shared/commit/29461b246b74423dcd39b0af4e1d140f1a3d3af1))
7
+
1
8
  # [@adobe/spacecat-shared-rum-api-client-v2.36.5](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.36.4...@adobe/spacecat-shared-rum-api-client-v2.36.5) (2025-08-25)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-rum-api-client",
3
- "version": "2.36.5",
3
+ "version": "2.37.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -38,8 +38,8 @@
38
38
  "@adobe/fetch": "4.2.2",
39
39
  "@adobe/helix-shared-wrap": "2.0.2",
40
40
  "@adobe/helix-universal": "5.2.2",
41
- "@adobe/spacecat-shared-utils": "1.26.4",
42
41
  "@adobe/rum-distiller": "1.17.0",
42
+ "@adobe/spacecat-shared-utils": "1.48.0",
43
43
  "aws4": "1.13.2",
44
44
  "urijs": "1.19.11"
45
45
  },
@@ -0,0 +1,97 @@
1
+ /*
2
+ * Copyright 2025 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 {
13
+ filterBundles,
14
+ initializeDataChunks,
15
+ extractMetrics,
16
+ createTimeSeriesData,
17
+ calculateTotals,
18
+ validateDateRange,
19
+ } from './utils.js';
20
+
21
+ /**
22
+ * Process URL-specific data
23
+ * @param {Object} urlFacet - URL facet from DataChunks
24
+ * @returns {Object} URL-specific data with totals and time series
25
+ */
26
+ function processUrlData(urlFacet) {
27
+ const urlBundles = urlFacet.entries;
28
+ const urlDataChunks = initializeDataChunks(urlBundles, { includeDateFacet: true });
29
+
30
+ return {
31
+ total: extractMetrics(urlFacet),
32
+ timeSeries: createTimeSeriesData(urlDataChunks.facets.date),
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Process bundles into aggregated graph data
38
+ * @param {Object[]} bundles - Array of RUM bundles
39
+ * @returns {Object} Aggregated traffic data
40
+ */
41
+ function processBundles(bundles) {
42
+ const dataChunks = initializeDataChunks(bundles, {
43
+ includeUrlFacet: true,
44
+ includeDateFacet: true,
45
+ });
46
+
47
+ // Process URL-specific data
48
+ const byUrl = {};
49
+ if (dataChunks.facets.url) {
50
+ dataChunks.facets.url.forEach((urlFacet) => {
51
+ byUrl[urlFacet.value] = processUrlData(urlFacet);
52
+ });
53
+ }
54
+
55
+ // Process overall traffic data
56
+ const trafficData = createTimeSeriesData(dataChunks.facets.date);
57
+
58
+ return { trafficData, byUrl };
59
+ }
60
+
61
+ /**
62
+ * Main handler function to generate graph data
63
+ * @param {Object[]} bundles - Array of RUM bundles
64
+ * @param {Object} opts - Options object
65
+ * @returns {Object}
66
+ */
67
+ function handler(bundles, opts) {
68
+ if (!opts) {
69
+ return {
70
+ trafficData: [],
71
+ byUrl: {},
72
+ totals: {},
73
+ urlsFiltered: [],
74
+ granularity: 'DAILY',
75
+ };
76
+ }
77
+
78
+ const {
79
+ startTime, endTime, urls = [],
80
+ } = opts;
81
+
82
+ validateDateRange(startTime, endTime);
83
+
84
+ const filteredBundles = filterBundles(bundles, opts);
85
+ const result = processBundles(filteredBundles);
86
+
87
+ return {
88
+ ...result,
89
+ totals: calculateTotals(result.trafficData),
90
+ urlsFiltered: urls,
91
+ granularity: opts.granularity || 'DAILY',
92
+ };
93
+ }
94
+
95
+ export default {
96
+ handler,
97
+ };
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright 2025 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 { initializeDataChunks, calculateMetrics, filterBundles } from './utils.js';
14
+
15
+ /**
16
+ * Handler for optimization reports metrics
17
+ * @param {Object[]} bundles - Array of RUM bundles
18
+ * @param {Object} opts - Options object
19
+ * @returns {Object} Calculated metrics
20
+ */
21
+ function handler(bundles, opts) {
22
+ const options = opts || {};
23
+ const filteredBundles = filterBundles(bundles, options);
24
+ const dataChunks = initializeDataChunks(filteredBundles);
25
+ return calculateMetrics(dataChunks);
26
+ }
27
+
28
+ export default {
29
+ handler,
30
+ };
@@ -0,0 +1,201 @@
1
+ /*
2
+ * Copyright 2025 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 { DataChunks, series, facets } from '@adobe/rum-distiller';
13
+ import { computeConversionRate } from '@adobe/rum-distiller/utils.js';
14
+ import { urlMatchesFilter } from '@adobe/spacecat-shared-utils';
15
+ import { loadBundles } from '../../../utils.js';
16
+
17
+ // Constants
18
+ const METRIC_NAMES = ['organic', 'visits', 'pageViews', 'bounces', 'conversions', 'engagement'];
19
+ const CONVERSION_SPEC = { checkpoint: ['click'] };
20
+
21
+ /**
22
+ * Create date facet function for YYYY-MM-DD format
23
+ * @returns {Function} Date facet function
24
+ */
25
+ function createDateFacet(bundle) {
26
+ const date = new Date(bundle.time);
27
+ return date.toISOString().split('T')[0];
28
+ }
29
+
30
+ /**
31
+ * Create conversion series function
32
+ * @param {DataChunks} dataChunks - DataChunks instance
33
+ * @returns {Function} Conversion series function
34
+ */
35
+ function createConversionSeries(dataChunks) {
36
+ return (bundle) => (bundle
37
+ && dataChunks.hasConversion(bundle, CONVERSION_SPEC) ? bundle.weight : 0);
38
+ }
39
+
40
+ /**
41
+ * Initialize DataChunks with common configuration
42
+ * @param {Object[]} bundles - Array of RUM bundles
43
+ * @param {Object} options - Configuration options
44
+ * @param {boolean} options.includeUrlFacet - Whether to include URL facet aggregation
45
+ * @param {boolean} options.includeDateFacet - Whether to include date facet aggregation
46
+ * @returns {DataChunks} Configured DataChunks instance
47
+ */
48
+ export function initializeDataChunks(bundles, options = {}) {
49
+ const { includeUrlFacet = false, includeDateFacet = false } = options;
50
+
51
+ const dataChunks = new DataChunks();
52
+
53
+ // Handle null/undefined bundles
54
+ const validBundles = (!bundles || !Array.isArray(bundles)) ? [] : bundles;
55
+
56
+ // Filter out bundles with missing or invalid URLs
57
+ const processedBundles = validBundles.filter((bundle) => bundle && bundle.url);
58
+
59
+ loadBundles(processedBundles, dataChunks);
60
+
61
+ // Add checkpoint facet for conversion detection
62
+ dataChunks.addFacet('checkpoint', facets.checkpoint, 'every', 'none');
63
+
64
+ // Add URL facet if requested
65
+ if (includeUrlFacet) {
66
+ dataChunks.addFacet('url', facets.url, 'some', 'none');
67
+ }
68
+
69
+ // Add date facet if requested
70
+ if (includeDateFacet) {
71
+ dataChunks.addFacet('date', createDateFacet);
72
+ }
73
+
74
+ // Add metrics series
75
+ dataChunks.addSeries('pageViews', series.pageViews);
76
+ dataChunks.addSeries('engagement', series.engagement);
77
+ dataChunks.addSeries('bounces', series.bounces);
78
+ dataChunks.addSeries('organic', series.organic);
79
+ dataChunks.addSeries('visits', series.visits);
80
+ dataChunks.addSeries('conversions', createConversionSeries(dataChunks));
81
+
82
+ return dataChunks;
83
+ }
84
+
85
+ /**
86
+ * Extract metrics from a facet
87
+ * @param {Object} facet - DataChunks facet
88
+ * @returns {Object} Metrics object
89
+ */
90
+ export function extractMetrics(facet) {
91
+ return METRIC_NAMES.reduce((acc, metric) => {
92
+ acc[metric] = facet.metrics[metric]?.sum || 0;
93
+ return acc;
94
+ }, {});
95
+ }
96
+
97
+ /**
98
+ * Create time series data from date facets
99
+ * @param {Object[]} dateFacets - Array of date facets
100
+ * @returns {Object[]} Sorted time series data
101
+ */
102
+ export function createTimeSeriesData(dateFacets) {
103
+ return dateFacets
104
+ .map((facet) => ({
105
+ date: facet.value,
106
+ ...extractMetrics(facet),
107
+ }))
108
+ .sort((a, b) => new Date(a.date) - new Date(b.date));
109
+ }
110
+
111
+ /**
112
+ * Calculate metrics from a DataChunks instance
113
+ * @param {DataChunks} chunk - DataChunks instance
114
+ * @returns {Object} Calculated metrics
115
+ */
116
+ export function calculateMetrics(chunk) {
117
+ const {
118
+ totals,
119
+ } = chunk;
120
+ return {
121
+ pageViews: { total: totals.pageViews?.sum || 0 },
122
+ visits: { total: totals.visits?.sum || 0 },
123
+ organicTraffic: { total: totals.organic?.sum || 0 },
124
+ bounces: {
125
+ total: totals.bounces?.sum || 0,
126
+ rate: computeConversionRate(totals.bounces?.sum || 0, totals.visits?.sum || 0) || 0,
127
+ },
128
+ engagement: {
129
+ total: totals.engagement?.sum || 0,
130
+ rate: computeConversionRate(totals.conversions?.sum || 0, totals.engagement?.sum || 0) || 0,
131
+ },
132
+ conversions: {
133
+ total: totals.conversions?.sum || 0,
134
+ rate: computeConversionRate(totals.conversions?.sum || 0, totals.pageViews?.sum || 0) || 0,
135
+ },
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Calculate totals from time series data
141
+ * @param {Object[]} timeSeriesData - Array of time series data points
142
+ * @returns {Object} Totals object
143
+ */
144
+ export function calculateTotals(timeSeriesData) {
145
+ return timeSeriesData.reduce((acc, data) => {
146
+ METRIC_NAMES.forEach((metric) => {
147
+ acc[metric] += (data[metric] || 0);
148
+ });
149
+ return acc;
150
+ }, METRIC_NAMES.reduce((acc, metric) => {
151
+ acc[metric] = 0;
152
+ return acc;
153
+ }, {}));
154
+ }
155
+
156
+ /**
157
+ * Filter bundles based on the outlierUrls and urls
158
+ * @param {Object[]} bundles - Array of RUM bundles
159
+ * @param {Object} opts - Options object
160
+ * @param {string[]} opts.outlierUrls - URLs to exclude
161
+ * @param {string[]} opts.urls - URLs to include
162
+ * @returns {Object[]} Filtered bundles
163
+ */
164
+ export function filterBundles(bundles, opts) {
165
+ // Handle null/undefined opts
166
+ const options = opts || {};
167
+
168
+ const {
169
+ outlierUrls,
170
+ urls,
171
+ } = options;
172
+
173
+ if (!bundles || !Array.isArray(bundles)) {
174
+ return [];
175
+ }
176
+
177
+ // Filter bundles by outlier URLs if provided
178
+ let filteredBundles = bundles;
179
+ if (outlierUrls && outlierUrls.length > 0) {
180
+ filteredBundles = filteredBundles
181
+ .filter((item) => item && item.url && !urlMatchesFilter(item.url, outlierUrls));
182
+ }
183
+
184
+ // If urls filter is provided, keep only those URLs
185
+ if (urls && urls.length > 0) {
186
+ filteredBundles = filteredBundles
187
+ .filter((item) => item && item.url && urlMatchesFilter(item.url, urls));
188
+ }
189
+ return filteredBundles;
190
+ }
191
+
192
+ /**
193
+ * Validate date range
194
+ * @param {string} startTime
195
+ * @param {string} endTime
196
+ */
197
+ export function validateDateRange(startTime, endTime) {
198
+ if (startTime && endTime && new Date(startTime) > new Date(endTime)) {
199
+ throw new Error('Start time must be before end time');
200
+ }
201
+ }
package/src/index.js CHANGED
@@ -25,6 +25,8 @@ import rageclick from './functions/opportunities/rageclick.js';
25
25
  import highInorganicHighBounceRate from './functions/opportunities/high-inorganic-high-bounce-rate.js';
26
26
  import highOrganicLowCtr from './functions/opportunities/high-organic-low-ctr.js';
27
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';
28
30
 
29
31
  // exported for tests
30
32
  export const RUM_BUNDLER_API_HOST = 'https://bundles.aem.page';
@@ -44,6 +46,8 @@ const HANDLERS = {
44
46
  pageviews,
45
47
  trafficMetrics,
46
48
  'traffic-analysis': trafficAnalysis,
49
+ 'optimization-report-metrics': optimizationReportMetrics,
50
+ 'optimization-report-graph': optimizationReportGraph,
47
51
  };
48
52
 
49
53
  function sanitize(opts) {