From edd84a80bf2e60f000fe0361b05172750b1c1937 Mon Sep 17 00:00:00 2001 From: Bhanu Dash Date: Wed, 7 Feb 2024 10:17:01 +0100 Subject: [PATCH] Initial versions --- .../src/index.js | 18 ++-- .../src/rumtoaamapper.js | 95 +++++++++++++++++++ .../test/aa-api-client.test.js | 26 ++--- 3 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 packages/spacecat-shared-aa-api-client/src/rumtoaamapper.js diff --git a/packages/spacecat-shared-aa-api-client/src/index.js b/packages/spacecat-shared-aa-api-client/src/index.js index 5114f6ed..c74c8e51 100644 --- a/packages/spacecat-shared-aa-api-client/src/index.js +++ b/packages/spacecat-shared-aa-api-client/src/index.js @@ -12,6 +12,9 @@ import { fetch } from './utils.js'; import { stringToUint8Array } from './helpers.js'; +const AA_SCOPE = 'openid,AdobeID,additional_info.projectedProductContext'; +const IMS_URL = 'https://ims-na1.adobelogin.com/ims/token/v3'; +const AA_URL = 'https://analytics-collection.adobe.io/aa/collect/v1'; function createBoundary() { return `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; } @@ -41,7 +44,7 @@ export default class AAAPIClient { #domain; constructor(config) { - ['IMS_URL', 'AA_CLIENT_ID', 'AA_CLIENT_SECRET', 'AA_SCOPES', 'AA_DOMAIN'].forEach((key) => { + ['AA_CLIENT_ID', 'AA_CLIENT_SECRET', 'AA_DOMAIN'].forEach((key) => { if (!config[key]) { throw new Error(`Missing required config: ${key}`); } @@ -51,12 +54,12 @@ export default class AAAPIClient { this.#domain = config.AA_DOMAIN; } - static create(context) { + static async create(context) { if (context.aaApiClient) { return context.aaApiClient; } context.aaApiClient = new AAAPIClient({ ...context.env }); - context.aaApiClient.#getIMSAccessToken(); + await context.aaApiClient.#getIMSAccessToken(); return context.aaApiClient; } @@ -79,8 +82,9 @@ export default class AAAPIClient { async #getIMSAccessToken() { const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; - const body = `client_id=${this.#config.AA_CLIENT_ID}&client_secret=${this.#config.AA_CLIENT_SECRET}&grant_type=client_credentials&scope=${this.#config.AA_SCOPES}`; - this.#token = await this.#post(this.#config.IMS_URL, headers, body); + const body = `client_id=${this.#config.AA_CLIENT_ID}&client_secret=${this.#config.AA_CLIENT_SECRET}&grant_type=client_credentials&scope=${AA_SCOPE}`; + const token = await this.#post(IMS_URL, headers, body); + this.#token = token.access_token; return this.#token; } @@ -95,7 +99,7 @@ export default class AAAPIClient { }; const multipartBody = createMultipartBody(archiveBuffer, zipPath, boundary); - return this.#post(`${this.#config.AA_URL}/events/validate`, headers, multipartBody); + return this.#post(`${AA_URL}/events/validate`, headers, multipartBody); } async ingestEvents(archiveBuffer, zipPath) { @@ -108,6 +112,6 @@ export default class AAAPIClient { 'Content-Type': `multipart/form-data; boundary=${boundary}`, }; const multipartBody = createMultipartBody(archiveBuffer, zipPath, boundary); - return this.#post(`${this.#config.AA_URL}/events`, headers, multipartBody); + return this.#post(`${AA_URL}/events`, headers, multipartBody); } } diff --git a/packages/spacecat-shared-aa-api-client/src/rumtoaamapper.js b/packages/spacecat-shared-aa-api-client/src/rumtoaamapper.js new file mode 100644 index 00000000..fc446bd8 --- /dev/null +++ b/packages/spacecat-shared-aa-api-client/src/rumtoaamapper.js @@ -0,0 +1,95 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { EVARS_KEY, EVENTS_KEY } from './helpers.js'; + +export default class RUMToAAMapper { + #config; + + constructor(config) { + this.#config = config; + } + + static getFieldValueOrDefault(field, defaultValue, data) { + if (field in data) { + return data[field]; + } + return defaultValue || ''; + } + + static formatAsCSV(entries) { + if (!entries || entries.length === 0) { + return ''; + } + + const headerFields = Object.keys(entries[0]); + + const rows = entries.map((row) => headerFields.map((fieldName) => JSON.stringify(row[fieldName])).join(',')); + + rows.unshift(headerFields.join(',')); + + return rows.join('\r\n'); + } + + mapRUMPageViewsToAA(rumData, date, timezone) { + const rumPagesList = rumData?.results?.data; + if (!rumPagesList || rumPagesList.length === 0) { + return []; + } + + return rumPagesList.map((rumPageView) => { + const mapping = this.#config.rum2aaMapping; + + const aaPageData = {}; + + Object.keys(mapping).forEach((key) => { + const keyMapping = mapping[key]; + + if (key === EVARS_KEY) { + Object.keys(keyMapping).forEach((evarKey) => { + const { rumField: field, default: defaultVal } = keyMapping[evarKey]; + aaPageData[evarKey] = RUMToAAMapper + .getFieldValueOrDefault(field, defaultVal, rumPageView); + }); + } else if (key === EVENTS_KEY) { + const events = []; + Object.keys(keyMapping).forEach((eventKey) => { + const { rumField: field, default: defaultVal } = keyMapping[eventKey]; + const fieldValue = RUMToAAMapper.getFieldValueOrDefault(field, defaultVal, rumPageView); + if (fieldValue) { + events.push(`${eventKey}=${fieldValue}`); + } + }); + + aaPageData[key] = events.length > 0 ? events.join(',') : ''; + } else { + const { rumField: field, default: defaultVal } = keyMapping; + const fieldValue = RUMToAAMapper.getFieldValueOrDefault(field, defaultVal, rumPageView); + + if (fieldValue) { + aaPageData[key] = fieldValue; + } + } + }); + + if (timezone === 'UTC') { + aaPageData.timestamp = `${date}T00:00:00Z`; + } else if (timezone.startsWith('UTC')) { + const offset = timezone.replace('UTC', ''); + aaPageData.timestamp = `${date}T00:00:00${offset}`; + } else { + aaPageData.timestamp = `${date}T00:00:00`; + } + + return aaPageData; + }).filter((aaPageData) => !!aaPageData[EVENTS_KEY]); // Skip record is no metric will be set + } +} diff --git a/packages/spacecat-shared-aa-api-client/test/aa-api-client.test.js b/packages/spacecat-shared-aa-api-client/test/aa-api-client.test.js index 594b3c7e..714b6f11 100644 --- a/packages/spacecat-shared-aa-api-client/test/aa-api-client.test.js +++ b/packages/spacecat-shared-aa-api-client/test/aa-api-client.test.js @@ -24,10 +24,8 @@ describe('Adobe Analytics api client', () => { beforeEach(() => { context = { env: { - IMS_URL: 'https://ims.com', AA_CLIENT_ID: 'test', - AA_CLIENT_SECRET: 'secret', - AA_SCOPES: 'test', + AA_CLIENT_SECRET: 'test', AA_DOMAIN: 'test', }, }; @@ -35,13 +33,19 @@ describe('Adobe Analytics api client', () => { afterEach('clean each', () => { nock.cleanAll(); }); - - it('does not create a new instance if previously initialized', async () => { - const aaApiClient = AAAPIClient.create({ aaApiClient: 'hebele', env: context.env }); - expect(aaApiClient).to.equal('hebele'); - }); - - it('rejects when one of the AA parameter missing', async () => { - expect(() => AAAPIClient.create(context)).to.throw('AA API Client needs a IMS_URL, AA_CLIENT_ID, AA_CLIENT_SECRET, AA_SCOPES, AA_DMAIN keys to be set'); + it('call validateFileFormat with valid file', async () => { + nock('https://ims-na1.adobelogin.com') + .post('/ims/token/v3') + .reply(200, { access_token: 'test' }); + const aaApiClient = await AAAPIClient.create(context); + const file = { + name: 'test.zip', + buffer: Buffer.from('test'), + }; + nock('https://analytics-collection.adobe.io') + .post('/aa/collect/v1/events/validate') + .reply(204, {}); + const result = await aaApiClient.validateFileFormat(file); + expect(result).to.throw; }); });