@adobe/spacecat-shared-athena-client 1.1.1 → 1.2.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-athena-client-v1.2.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-athena-client-v1.2.0...@adobe/spacecat-shared-athena-client-v1.2.1) (2025-07-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * mapping of traffic data response ([#871](https://github.com/adobe/spacecat-shared/issues/871)) ([ca65936](https://github.com/adobe/spacecat-shared/commit/ca659361b2f61c83bf995c285860d479e9f82066))
7
+
8
+ # [@adobe/spacecat-shared-athena-client-v1.2.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-athena-client-v1.1.1...@adobe/spacecat-shared-athena-client-v1.2.0) (2025-07-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * ensure queries are shared so cache and api return same ([#866](https://github.com/adobe/spacecat-shared/issues/866)) ([b23d772](https://github.com/adobe/spacecat-shared/commit/b23d772a3a04355333ed090cc00386c33db6f872))
14
+
1
15
  # [@adobe/spacecat-shared-athena-client-v1.1.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-athena-client-v1.1.0...@adobe/spacecat-shared-athena-client-v1.1.1) (2025-07-19)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-athena-client",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Shared modules of the Spacecat Services - AWS Athena Client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@aws-sdk/client-athena": "3.848.0",
39
- "@adobe/spacecat-shared-utils": "1.41.3"
39
+ "@adobe/spacecat-shared-utils": "1.43.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "chai": "5.2.1",
package/src/index.js CHANGED
@@ -256,4 +256,11 @@ export class AWSAthenaClient {
256
256
  }
257
257
  }
258
258
 
259
- export { getTrafficAnalysisQuery, getTrafficAnalysisQueryPlaceholders } from './queries.js';
259
+ export {
260
+ getTrafficAnalysisQuery,
261
+ getTrafficAnalysisQueryPlaceholders,
262
+ buildPageTypeCase,
263
+ getTrafficAnalysisQueryPlaceholdersFilled,
264
+ } from './traffic-analysis/queries.js';
265
+ export { TrafficDataResponseDto } from './traffic-analysis/traffic-data-base-response.js';
266
+ export { TrafficDataWithCWVDto } from './traffic-analysis/traffic-data-with-cwv.js';
@@ -0,0 +1,115 @@
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 { getStaticContent, getDateRanges } from '@adobe/spacecat-shared-utils';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const currentFile = fileURLToPath(import.meta.url);
17
+ const currentDir = path.dirname(currentFile);
18
+
19
+ const TRAFFIC_ANALYSIS_PATH = path.resolve(currentDir, '../../static/queries/traffic-analysis.sql.tpl');
20
+
21
+ /**
22
+ * Loads the traffic analysis query template and applies placeholders.
23
+ * @param {Object} placeholders - Key-value pairs to replace in the query template.
24
+ * @param {Object} log - Logger (optional)
25
+ * @returns {Promise<string|null>} The templated SQL string or null on error.
26
+ */
27
+ export async function getTrafficAnalysisQuery(placeholders = {}) {
28
+ return getStaticContent(placeholders, TRAFFIC_ANALYSIS_PATH);
29
+ }
30
+
31
+ /**
32
+ * Scans the query template and returns a sorted array of unique placeholder (strings).
33
+ * @returns {Promise<string[]>} Array of unique placeholder keys found in the template.
34
+ */
35
+ export async function getTrafficAnalysisQueryPlaceholders() {
36
+ const raw = await getStaticContent({}, TRAFFIC_ANALYSIS_PATH);
37
+ const matches = raw.match(/{{\s*([\w]+)\s*}}/g);
38
+ return [...new Set((matches).map((m) => m.replace(/{{\s*|\s*}}/g, '')))].sort();
39
+ }
40
+
41
+ /**
42
+ * Returns case statement for pattern matching field to pageTypes
43
+ * @param {list} pageTypes - List containing name and pattern to use
44
+ * @param {string} column - Column to use for mapping.
45
+ * @returns {string|null} - Athena SQL case statement to project new column based on patterns.
46
+ */
47
+ export function buildPageTypeCase(pageTypes, column) {
48
+ if (!pageTypes || !pageTypes.length) {
49
+ return null;
50
+ }
51
+
52
+ const caseLines = [
53
+ 'CASE',
54
+ ...pageTypes.map(({ name, pattern }) => ` WHEN REGEXP_LIKE(${column}, '${pattern}') THEN '${name.replace(/'/g, "''")}'`),
55
+ " ELSE 'other | Other Pages'",
56
+ 'END AS page_type',
57
+ ];
58
+
59
+ return caseLines.join('\n');
60
+ }
61
+
62
+ /**
63
+ * Builds the placeholder values for the traffic analysis SQL template.
64
+ *
65
+ * @param {Object} params - Input parameters.
66
+ * @param {number} params.week - The ISO week number (1–53).
67
+ * @param {number} params.year - The year (e.g. 2025).
68
+ * @param {string} params.siteId - UUID of the site.
69
+ * @param {string[]} [params.dimensions- Dimensions to group by (e.g. ['utm_campaign', 'device']).
70
+ * @param {string} params.tableName - The name of the source table.
71
+ * @param {string} params.pageTypeMatchColumn - The pageTypeMatchColumn of the source table.
72
+ * @param {Object|null} [params.pageTypes=null] - Optional pageType rules for CASE generation.
73
+ *
74
+ * @returns {Object} Template values for SQL generation.
75
+ */
76
+ export function getTrafficAnalysisQueryPlaceholdersFilled({
77
+ week,
78
+ year,
79
+ siteId,
80
+ dimensions,
81
+ tableName,
82
+ pageTypes = null,
83
+ pageTypeMatchColumn = 'path',
84
+ }) {
85
+ if (!week || !year || !siteId || !tableName) {
86
+ throw new Error('Missing required parameters: week, year, siteId, or tableName');
87
+ }
88
+
89
+ if (!Array.isArray(dimensions) || dimensions.length === 0) {
90
+ throw new Error('Missing dimension to group by');
91
+ }
92
+
93
+ const dimensionColumns = dimensions.join(', ');
94
+ const dimensionColumnsPrefixed = dimensions.map((col) => `a.${col}`).join(', ');
95
+
96
+ const dateRanges = getDateRanges(week, year);
97
+ const temporalCondition = dateRanges
98
+ .map((r) => `(year=${r.year} AND month=${r.month} AND week=${week})`)
99
+ .join(' OR ');
100
+
101
+ let pageTypeCase = 'NULL as page_type';
102
+ if (dimensions.includes('page_type') && pageTypes) {
103
+ pageTypeCase = buildPageTypeCase(pageTypes, pageTypeMatchColumn);
104
+ }
105
+
106
+ return {
107
+ siteId,
108
+ groupBy: dimensionColumns,
109
+ dimensionColumns,
110
+ dimensionColumnsPrefixed,
111
+ tableName,
112
+ temporalCondition,
113
+ pageTypeCase,
114
+ };
115
+ }
@@ -0,0 +1,51 @@
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
+ /**
14
+ * Data transfer object for Marketing Channel Response.
15
+ */
16
+ export const TrafficDataResponseDto = {
17
+ /**
18
+ * Converts a traffic data object into a JSON object.
19
+ * @param {object} data - traffic data object.
20
+ * @returns {{
21
+ * type: string,
22
+ * channel: string,
23
+ * platform: string,
24
+ * campaign: string,
25
+ * referrer: string,
26
+ * pageviews: number,
27
+ * pct_pageviews: number,
28
+ * click_rate: number,
29
+ * engagement_rate: number,
30
+ * bounce_rate: number,
31
+ * engaged_scroll: number,
32
+ * p70_scroll: number,
33
+ * }} JSON object.
34
+ */
35
+ toJSON: (data) => ({
36
+ type: data.trf_type,
37
+ channel: data.trf_channel,
38
+ platform: data.trf_platform,
39
+ utm_source: data.utm_source,
40
+ utm_medium: data.utm_medium,
41
+ campaign: data.utm_campaign,
42
+ referrer: data.referrer,
43
+ pageviews: data.pageviews,
44
+ pct_pageviews: data.pct_pageviews,
45
+ click_rate: data.click_rate,
46
+ engagement_rate: data.engagement_rate,
47
+ bounce_rate: data.bounce_rate,
48
+ engaged_scroll: data.engaged_scroll,
49
+ p70_scroll: data.p70_scroll,
50
+ }),
51
+ };
@@ -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
+
13
+ /* eslint-disable camelcase */
14
+ import { TrafficDataResponseDto } from './traffic-data-base-response.js';
15
+
16
+ const DEFAULT_THRESHOLDS = {
17
+ LCP_GOOD: 2500,
18
+ LCP_NEEDS_IMPROVEMENT: 4000,
19
+ INP_GOOD: 200,
20
+ INP_NEEDS_IMPROVEMENT: 500,
21
+ CLS_GOOD: 0.1,
22
+ CLS_NEEDS_IMPROVEMENT: 0.25,
23
+ };
24
+
25
+ function getThreshold(key, config) {
26
+ return key in config ? config[key] : DEFAULT_THRESHOLDS[key];
27
+ }
28
+
29
+ function scoreCWV(metric, val, config) {
30
+ const GOOD = getThreshold(`${metric}_GOOD`, config);
31
+ const NEEDS_IMPROVEMENT = getThreshold(`${metric}_NEEDS_IMPROVEMENT`, config);
32
+
33
+ if (val <= GOOD) return 'good';
34
+ if (val <= NEEDS_IMPROVEMENT) return 'needs improvement';
35
+ return 'poor';
36
+ }
37
+
38
+ /**
39
+ * Converts marketing traffic data into enriched DTO with CWV scores.
40
+ */
41
+ export const TrafficDataWithCWVDto = {
42
+ /**
43
+ * @param {object} data - Raw data input.
44
+ * @param {object} [thresholdConfig] - Optional override for CWV thresholds.
45
+ * @returns {{
46
+ * type: string,
47
+ * channel: string,
48
+ * campaign: string,
49
+ * pageviews: number,
50
+ * pct_pageviews: number,
51
+ * click_rate: number,
52
+ * engagement_rate: number,
53
+ * bounce_rate: number,
54
+ * p70_lcp: number,
55
+ * p70_cls: number,
56
+ * p70_inp: number,
57
+ * lcp_score: string,
58
+ * inp_score: string,
59
+ * cls_score: string,
60
+ * overall_cwv_score: string,
61
+ * }} JSON object.
62
+ */
63
+ toJSON: (data, thresholdConfig, baseUrl) => {
64
+ const lcp = Number(data.p70_lcp);
65
+ const inp = Number(data.p70_inp);
66
+ const cls = Number(data.p70_cls);
67
+
68
+ const lcp_score = scoreCWV('LCP', lcp, thresholdConfig);
69
+ const inp_score = scoreCWV('INP', inp, thresholdConfig);
70
+ const cls_score = scoreCWV('CLS', cls, thresholdConfig);
71
+
72
+ const scores = [lcp_score, inp_score, cls_score];
73
+ let overall_cwv_score;
74
+ if (scores.includes('poor')) {
75
+ overall_cwv_score = 'poor';
76
+ } else if (scores.includes('needs improvement')) {
77
+ overall_cwv_score = 'needs improvement';
78
+ } else {
79
+ overall_cwv_score = 'good';
80
+ }
81
+
82
+ return {
83
+ ...TrafficDataResponseDto.toJSON(data),
84
+ url: data.url || (baseUrl && data.path ? `${baseUrl.replace(/\/$/, '')}/${data.path.replace(/^\//, '')}` : undefined),
85
+ path: data.path,
86
+ page_type: data.page_type,
87
+ device: data.device,
88
+ p70_lcp: lcp,
89
+ p70_cls: cls,
90
+ p70_inp: inp,
91
+ lcp_score,
92
+ inp_score,
93
+ cls_score,
94
+ overall_cwv_score,
95
+ };
96
+ },
97
+ };
@@ -1,55 +1,59 @@
1
1
  WITH raw AS (
2
2
  SELECT
3
- trf_type,
4
3
  path,
5
- {{pageTypeCase}},
4
+ {{pageTypeCase}},
5
+ trf_type,
6
6
  trf_channel,
7
- utm_campaign,
8
7
  trf_platform,
9
8
  device,
9
+ utm_source,
10
+ utm_medium,
11
+ utm_campaign,
12
+ referrer,
13
+ consent,
14
+ notfound,
10
15
  pageviews,
11
16
  clicked,
12
17
  engaged,
18
+ latest_scroll,
19
+ CASE WHEN latest_scroll >= 10000 THEN 1 ELSE 0 END AS engaged_scroll,
13
20
  lcp,
14
21
  cls,
15
22
  inp
16
23
  FROM {{tableName}}
17
24
  WHERE siteid = '{{siteId}}'
18
- AND ({{temporalCondition}})
25
+ AND ({{temporalCondition}})
19
26
  ),
20
-
21
27
  agg AS (
22
28
  SELECT
23
29
  {{dimensionColumns}},
24
30
  COUNT(*) AS row_count,
25
- CAST(SUM(pageviews) AS BIGINT) AS pageviews,
26
- CAST(SUM(clicked) AS BIGINT) AS clicks,
27
- CAST(SUM(engaged) AS BIGINT) AS engagements,
28
- approx_percentile(lcp, 0.70) AS p70_lcp,
29
- approx_percentile(cls, 0.70) AS p70_cls,
30
- approx_percentile(inp, 0.70) AS p70_inp
31
+ CAST(SUM(pageviews) AS BIGINT) AS pageviews,
32
+ CAST(SUM(clicked) AS BIGINT) AS clicks,
33
+ CAST(SUM(engaged) AS BIGINT) AS engagements,
34
+ CAST(SUM(engaged_scroll) AS BIGINT) AS engaged_scroll,
35
+ approx_percentile(latest_scroll, 0.70) AS p70_scroll,
36
+ approx_percentile(lcp, 0.70) AS p70_lcp,
37
+ approx_percentile(cls, 0.70) AS p70_cls,
38
+ approx_percentile(inp, 0.70) AS p70_inp
31
39
  FROM raw
32
- GROUP BY
33
- {{groupBy}}
40
+ GROUP BY {{groupBy}}
34
41
  ),
35
-
36
42
  grand_total AS (
37
43
  SELECT CAST(SUM(pageviews) AS BIGINT) AS total_pv FROM agg
38
44
  )
39
-
40
45
  SELECT
41
46
  {{dimensionColumnsPrefixed}},
42
47
  a.pageviews,
43
-
44
- CAST(a.pageviews AS double) / NULLIF(t.total_pv, 0) AS pct_pageviews,
45
- CAST(a.clicks AS double) / NULLIF(a.row_count, 0) AS click_rate,
46
- CAST(a.engagements AS double) / NULLIF(a.row_count, 0) AS engagement_rate,
47
- 1 - CAST(a.engagements AS double) / NULLIF(a.row_count, 0) AS bounce_rate,
48
-
48
+ CAST(a.pageviews AS DOUBLE) / NULLIF(t.total_pv, 0) AS pct_pageviews,
49
+ CAST(a.clicks AS DOUBLE) / NULLIF(a.row_count, 0) AS click_rate,
50
+ CAST(a.engagements AS DOUBLE) / NULLIF(a.row_count, 0) AS engagement_rate,
51
+ 1 - CAST(a.engagements AS DOUBLE) / NULLIF(a.row_count, 0) AS bounce_rate,
52
+ a.engaged_scroll,
53
+ a.p70_scroll,
49
54
  a.p70_lcp,
50
55
  a.p70_cls,
51
56
  a.p70_inp
52
-
53
57
  FROM agg a
54
58
  CROSS JOIN grand_total t
55
59
  ORDER BY a.pageviews DESC
package/src/queries.js DELETED
@@ -1,34 +0,0 @@
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 { getStaticContent } from '@adobe/spacecat-shared-utils';
13
-
14
- const TRAFIC_ANALYSIS_PATH = 'static/queries/traffic-analysis.sql.tpl';
15
-
16
- /**
17
- * Loads the traffic analysis query template and applies placeholders.
18
- * @param {Object} placeholders - Key-value pairs to replace in the query template.
19
- * @param {Object} log - Logger (optional)
20
- * @returns {Promise<string|null>} The templated SQL string or null on error.
21
- */
22
- export async function getTrafficAnalysisQuery(placeholders = {}) {
23
- return getStaticContent(placeholders, TRAFIC_ANALYSIS_PATH);
24
- }
25
-
26
- /**
27
- * Scans the query template and returns a sorted array of unique placeholder (strings).
28
- * @returns {Promise<string[]>} Array of unique placeholder keys found in the template.
29
- */
30
- export async function getTrafficAnalysisQueryPlaceholders() {
31
- const raw = await getStaticContent({}, TRAFIC_ANALYSIS_PATH);
32
- const matches = raw.match(/{{\s*([\w]+)\s*}}/g);
33
- return [...new Set((matches).map((m) => m.replace(/{{\s*|\s*}}/g, '')))].sort();
34
- }