diff --git a/packages/target-decisioning-engine/src/artifactProvider.spec.js b/packages/target-decisioning-engine/src/artifactProvider.spec.js index 0b2f532c..5aefdfe5 100644 --- a/packages/target-decisioning-engine/src/artifactProvider.spec.js +++ b/packages/target-decisioning-engine/src/artifactProvider.spec.js @@ -1,6 +1,10 @@ /* eslint-disable jest/no-test-callback */ import * as HttpStatus from "http-status-codes"; -import { ENVIRONMENT_PROD, ENVIRONMENT_STAGE } from "@adobe/target-tools"; +import { + ENVIRONMENT_PROD, + ENVIRONMENT_STAGE, + isDefined +} from "@adobe/target-tools"; import ArtifactProvider from "./artifactProvider"; import * as constants from "./constants"; import { @@ -36,7 +40,9 @@ describe("artifactProvider", () => { }); afterEach(() => { - provider.stopPolling(); + if (isDefined(provider)) { + provider.stopPolling(); + } provider = undefined; }); @@ -134,6 +140,8 @@ describe("artifactProvider", () => { // eslint-disable-next-line jest/no-test-callback it("reports an error if it failed to retrieve the artifact after 10 tries", async () => { + expect.assertions(3); + fetch.mockResponses( ["", { status: HttpStatus.UNAUTHORIZED }], ["", { status: HttpStatus.NOT_FOUND }], @@ -283,24 +291,25 @@ describe("artifactProvider", () => { it("emits artifactDownloadSucceeded event", async done => { fetch.mockResponse(JSON.stringify(DUMMY_ARTIFACT_PAYLOAD)); - expect.assertions(3); + expect.assertions(2); function eventEmitter(eventName, payload) { expect(eventName).toEqual(ARTIFACT_DOWNLOAD_SUCCEEDED); - expect(payload).toEqual( - expect.objectContaining({ - artifactLocation: - "https://assets.adobetarget.com/clientId/production/v1/rules.bin", - artifactPayload: expect.any(Object) - }) - ); - setTimeout(() => done(), 100); + expect(payload).toMatchObject({ + artifactLocation: + "https://assets.adobetarget.com/clientId/production/v1/rules.bin", + artifactPayload: expect.any(Object) + }); + done(); } provider = await ArtifactProvider({ ...TEST_CONF, pollingInterval: 0, - eventEmitter + eventEmitter: (eventName, payload) => + eventName === ARTIFACT_DOWNLOAD_SUCCEEDED + ? eventEmitter(eventName, payload) + : undefined }); }); @@ -340,7 +349,7 @@ describe("determineArtifactLocation", () => { cdnEnvironment: "staging" }) ).toEqual( - `${CDN_BASE_STAGE}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_STAGE}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` ); }); @@ -350,7 +359,7 @@ describe("determineArtifactLocation", () => { client: "someClientId" }) ).toEqual( - `${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` ); }); @@ -361,7 +370,7 @@ describe("determineArtifactLocation", () => { environment: ENVIRONMENT_STAGE }) ).toEqual( - `${CDN_BASE_PROD}/someClientId/${ENVIRONMENT_STAGE}/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_PROD}/someClientId/${ENVIRONMENT_STAGE}/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` ); }); @@ -380,7 +389,7 @@ describe("determineArtifactLocation", () => { } }) ).toEqual( - `${CDN_BASE_PROD}/someClientId/${ENVIRONMENT_PROD}/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_PROD}/someClientId/${ENVIRONMENT_PROD}/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` ); }); @@ -391,7 +400,7 @@ describe("determineArtifactLocation", () => { propertyToken: "xyz-123-abc" }) ).toEqual( - `${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/${ARTIFACT_FILENAME}` ); }); @@ -405,7 +414,7 @@ describe("determineArtifactLocation", () => { true ) ).toEqual( - `${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/xyz-123-abc/${ARTIFACT_FILENAME}` + `https://${CDN_BASE_PROD}/someClientId/production/v${SUPPORTED_ARTIFACT_MAJOR_VERSION}/xyz-123-abc/${ARTIFACT_FILENAME}` ); }); }); diff --git a/packages/target-decisioning-engine/src/constants.js b/packages/target-decisioning-engine/src/constants.js index bf6d3e24..ca3d96c6 100644 --- a/packages/target-decisioning-engine/src/constants.js +++ b/packages/target-decisioning-engine/src/constants.js @@ -16,9 +16,9 @@ export const ARTIFACT_FORMAT_JSON = "json"; export const LOG_PREFIX = "LD"; -export const CDN_BASE_PROD = "https://assets.adobetarget.com"; -export const CDN_BASE_STAGE = "https://assets.staging.adobetarget.com"; -export const CDN_BASE_DEV = "https://assets.staging.adobetarget.com"; +export const CDN_BASE_PROD = "assets.adobetarget.com"; +export const CDN_BASE_STAGE = "assets.staging.adobetarget.com"; +export const CDN_BASE_DEV = "assets.staging.adobetarget.com"; export const HTTP_HEADER_FORWARDED_FOR = "x-forwarded-for"; export const HTTP_HEADER_GEO_LATITUDE = "x-geo-latitude"; diff --git a/packages/target-decisioning-engine/src/index.js b/packages/target-decisioning-engine/src/index.js index 96587db5..1ec9c80d 100644 --- a/packages/target-decisioning-engine/src/index.js +++ b/packages/target-decisioning-engine/src/index.js @@ -1,10 +1,4 @@ -import { - DEFAULT_MAXIMUM_WAIT_READY, - getLogger, - isDefined, - isUndefined, - whenReady -} from "@adobe/target-tools"; +import { getLogger, isDefined, isUndefined } from "@adobe/target-tools"; import { createDecisioningContext } from "./contextProvider"; import DecisionProvider from "./decisionProvider"; import ArtifactProvider from "./artifactProvider"; @@ -20,24 +14,10 @@ import { GeoProvider } from "./geoProvider"; * @param {import("../types/DecisioningConfig").DecisioningConfig} config Options map, required */ export default function TargetDecisioningEngine(config) { - const { maximumWaitReady = DEFAULT_MAXIMUM_WAIT_READY } = config; const logger = getLogger(config.logger); let artifactProvider; let artifact; - ArtifactProvider({ - ...config, - logger - }).then(providerInstance => { - artifactProvider = providerInstance; - artifact = artifactProvider.getArtifact(); - - // subscribe to new artifacts that are downloaded on the polling interval - artifactProvider.subscribe(data => { - artifact = data; - }); - }); - /** * The get offers method * @param {import("../types/TargetOptions").TargetOptions} targetOptions @@ -95,13 +75,22 @@ export default function TargetDecisioningEngine(config) { return isDefined(artifact); } - const whenArtifactReady = whenReady( - isReady, - maximumWaitReady, - Messages.ARTIFACT_NOT_AVAILABLE - ); + return ArtifactProvider({ + ...config, + logger + }).then(providerInstance => { + artifactProvider = providerInstance; + artifact = artifactProvider.getArtifact(); + + if (isUndefined(artifact)) { + throw new Error(Messages.ARTIFACT_NOT_AVAILABLE); + } + + // subscribe to new artifacts that are downloaded on the polling interval + artifactProvider.subscribe(data => { + artifact = data; + }); - return whenArtifactReady.then(() => { return { getRawArtifact: () => artifact, stopPolling: () => artifactProvider.stopPolling(), diff --git a/packages/target-decisioning-engine/src/messages.js b/packages/target-decisioning-engine/src/messages.js index c18cd133..d2f4eb7a 100644 --- a/packages/target-decisioning-engine/src/messages.js +++ b/packages/target-decisioning-engine/src/messages.js @@ -5,6 +5,7 @@ const Messages = { ARTIFACT_VERSION_UNSUPPORTED: (artifactVersion, supportedMajorVersion) => `The decisioning artifact version (${artifactVersion}) is not supported. This library is compatible with this major version: ${supportedMajorVersion}`, ARTIFACT_FETCH_ERROR: reason => `Failed to retrieve artifact: ${reason}`, + ARTIFACT_INVALID: "Invalid Artifact", INVALID_ENVIRONMENT: (expectedEnvironment, defaultEnvironment) => `'${expectedEnvironment}' is not a valid target environment, defaulting to '${defaultEnvironment}'.`, NOT_APPLICABLE: "Not Applicable", diff --git a/packages/target-decisioning-engine/src/obfuscationProvider.js b/packages/target-decisioning-engine/src/obfuscationProvider.js index efa41dff..395065fa 100644 --- a/packages/target-decisioning-engine/src/obfuscationProvider.js +++ b/packages/target-decisioning-engine/src/obfuscationProvider.js @@ -63,7 +63,7 @@ function ObfuscationProvider(config) { const header = getHeader(buffer.slice(0, HEADER_BOUNDARY)); if (header.version !== SUPPORTED_ARTIFACT_OBFUSCATION_VERSION) { - throw new Error("Invalid Artifact"); + throw new Error(Messages.ARTIFACT_INVALID); } return getArtifact(header.key, buffer.slice(HEADER_BOUNDARY)); diff --git a/packages/target-decisioning-engine/src/utils.js b/packages/target-decisioning-engine/src/utils.js index 7026c1a1..6977e622 100644 --- a/packages/target-decisioning-engine/src/utils.js +++ b/packages/target-decisioning-engine/src/utils.js @@ -180,13 +180,18 @@ export function getCdnEnvironment(config) { * @return {string} */ export function getCdnBasePath(config) { - const cdnEnvironment = getCdnEnvironment(config); + let { cdnBasePath } = config; - const env = includes(cdnEnvironment, POSSIBLE_ENVIRONMENTS) - ? cdnEnvironment - : ENVIRONMENT_PROD; + if (!isDefined(cdnBasePath)) { + const cdnEnvironment = getCdnEnvironment(config); - return CDN_BASE[env]; + const env = includes(cdnEnvironment, POSSIBLE_ENVIRONMENTS) + ? cdnEnvironment + : ENVIRONMENT_PROD; + cdnBasePath = CDN_BASE[env]; + } + + return `https://${cdnBasePath}`; } /** diff --git a/packages/target-decisioning-engine/types/DecisioningConfig.d.ts b/packages/target-decisioning-engine/types/DecisioningConfig.d.ts index 7380a23e..5b4eab13 100644 --- a/packages/target-decisioning-engine/types/DecisioningConfig.d.ts +++ b/packages/target-decisioning-engine/types/DecisioningConfig.d.ts @@ -46,6 +46,11 @@ export interface DecisioningConfig { */ cdnEnvironment?: String; + /** + * A CDN base URL to override the default based on cdnEnvironment. + */ + cdnBasePath?: String; + /** * Replaces the default noop logger */ diff --git a/packages/target-nodejs-sdk/src/index.js b/packages/target-nodejs-sdk/src/index.js index 1a1fc136..97f55f8d 100644 --- a/packages/target-nodejs-sdk/src/index.js +++ b/packages/target-nodejs-sdk/src/index.js @@ -71,6 +71,7 @@ export default function bootstrap(fetchApi) { propertyToken: options.propertyToken, environment: options.environment, cdnEnvironment: options.cdnEnvironment, + cdnBasePath: options.cdnBasePath, logger: this.logger, fetchApi: fetchImpl, eventEmitter, @@ -185,6 +186,7 @@ export default function bootstrap(fetchApi) { * @param {Array} options.customerIds An array of Customer Ids in VisitorId-compatible format, optional * @param {String} options.sessionId Session Id, used for linking multiple requests, optional * @param {Object} options.visitor Supply an external VisitorId instance, optional + * @param {('on-device'|'server-side'|'hybrid')} options.decisioningMethod The execution mode, defaults to remote, optional */ getAttributes(mboxNames, options = {}) { // eslint-disable-next-line no-param-reassign diff --git a/packages/target-nodejs-sdk/src/target.js b/packages/target-nodejs-sdk/src/target.js index 51ee3258..0c47555b 100644 --- a/packages/target-nodejs-sdk/src/target.js +++ b/packages/target-nodejs-sdk/src/target.js @@ -89,8 +89,6 @@ export function executeDelivery(options, decisioningEngine) { const deliveryRequest = createDeliveryRequest(request, requestOptions); - logger.debug(Messages.REQUEST_SENT, JSON.stringify(deliveryRequest, null, 2)); - const configuration = createConfiguration( fetchWithRetry, host, @@ -108,6 +106,12 @@ export function executeDelivery(options, decisioningEngine) { decisioningEngine ); + logger.debug( + Messages.REQUEST_SENT, + deliveryMethod.decisioningMethod, + JSON.stringify(deliveryRequest, null, 2) + ); + return deliveryMethod .execute(client, sessionId, deliveryRequest, config.version) .then((response = {}) => { diff --git a/packages/target-tools/src/index.js b/packages/target-tools/src/index.js index abcd31b5..776522a9 100644 --- a/packages/target-tools/src/index.js +++ b/packages/target-tools/src/index.js @@ -94,4 +94,6 @@ export { PROPERTY_TOKEN_MISMATCH } from "./messages"; +export { perfTool } from "./perftool"; + export { default as parseURI } from "parse-uri"; diff --git a/packages/target-tools/src/perftool.js b/packages/target-tools/src/perftool.js new file mode 100644 index 00000000..3745c4a2 --- /dev/null +++ b/packages/target-tools/src/perftool.js @@ -0,0 +1,44 @@ +/* eslint-disable import/prefer-default-export */ +import { now } from "./lodash"; +import { isDefined, isUndefined } from "./utils"; + +let timingIds = {}; +let startTimes = {}; +let timings = {}; + +function getUniqueTimingId(id) { + const count = (isDefined(timingIds[id]) ? timingIds[id] : 0) + 1; + timingIds[id] = count; + + return `${id}${count}`; +} + +function timeStart(id, incrementTimer = false) { + const timingId = incrementTimer ? getUniqueTimingId(id) : id; + if (isUndefined(startTimes[timingId])) { + startTimes[timingId] = now(); + } + return timingId; +} + +function timeEnd(id, offset = 0) { + if (isUndefined(startTimes[id])) return `No timer was started for "${id}"`; + + const timing = now() - startTimes[id] - offset; + timings[id] = timing; + return timing; +} + +function reset() { + timingIds = {}; + startTimes = {}; + timings = {}; +} + +export const perfTool = { + timeStart, + timeEnd, + getTimings: () => timings, + getTiming: key => timings[key], + reset +}; diff --git a/packages/target-tools/src/perftool.spec.js b/packages/target-tools/src/perftool.spec.js new file mode 100644 index 00000000..3cf45436 --- /dev/null +++ b/packages/target-tools/src/perftool.spec.js @@ -0,0 +1,89 @@ +import { perfTool } from "./perftool"; + +describe("perfTool", () => { + beforeEach(() => { + perfTool.reset(); + }); + + it("can time", () => { + return new Promise(done => { + perfTool.timeStart("moo"); + setTimeout(() => { + perfTool.timeEnd("moo"); + expect(perfTool.getTimings()).toEqual({ + moo: expect.any(Number) + }); + expect(perfTool.getTimings().moo).toBeGreaterThanOrEqual(100); + done(); + }, 100); + }); + }); + + it("can time with offset", () => { + return new Promise(done => { + perfTool.timeStart("moo"); + setTimeout(() => { + perfTool.timeEnd("moo", 50); + expect(perfTool.getTimings()).toEqual({ + moo: expect.any(Number) + }); + expect(perfTool.getTimings().moo).toBeLessThan(100); + done(); + }, 100); + }); + }); + + it("can time many", () => { + return new Promise(done => { + perfTool.timeStart("moo"); + perfTool.timeStart("meow"); + perfTool.timeStart("woof"); + + setTimeout(() => perfTool.timeEnd("moo"), 100); + setTimeout(() => perfTool.timeEnd("meow"), 200); + setTimeout(() => perfTool.timeEnd("woof"), 300); + + setTimeout(() => { + expect(perfTool.getTimings()).toEqual({ + moo: expect.any(Number), + meow: expect.any(Number), + woof: expect.any(Number) + }); + expect(perfTool.getTimings().moo).toBeGreaterThanOrEqual(100); + expect(perfTool.getTimings().meow).toBeGreaterThanOrEqual(200); + expect(perfTool.getTimings().woof).toBeGreaterThanOrEqual(300); + done(); + }, 300); + }); + }); + + it("fails gracefully", () => { + expect(perfTool.timeEnd("bleh")).toEqual('No timer was started for "bleh"'); + }); + + it("can time repeats", () => { + return new Promise(done => { + const firstTime = perfTool.timeStart("moo", true); + const secondTime = perfTool.timeStart("moo", true); + const thirdTime = perfTool.timeStart("moo", true); + + expect(firstTime).toEqual("moo1"); + expect(secondTime).toEqual("moo2"); + expect(thirdTime).toEqual("moo3"); + + setTimeout(() => { + perfTool.timeEnd(firstTime); + perfTool.timeEnd(secondTime); + perfTool.timeEnd(thirdTime); + + const timings = perfTool.getTimings(); + expect(timings).toEqual({ + moo1: expect.any(Number), + moo2: expect.any(Number), + moo3: expect.any(Number) + }); + done(); + }, 100); + }); + }); +}); diff --git a/packages/target-tools/src/utils.js b/packages/target-tools/src/utils.js index a4365667..c046a284 100644 --- a/packages/target-tools/src/utils.js +++ b/packages/target-tools/src/utils.js @@ -230,18 +230,20 @@ export function whenReady( errorMessage ) { const initTime = now(); + let timer; return new Promise((resolve, reject) => { - (function wait(count) { + function wait() { if (timeLimitExceeded(initTime, maximumWaitTime)) { + clearTimeout(timer); reject(new Error(errorMessage)); return; } if (isReady()) { + clearTimeout(timer); resolve(); - return; } - setTimeout(() => wait(count + 1), 100); - })(0); + } + timer = setInterval(() => wait(), 0); }); }