From f7e8fa677143a7b6a3867846b033ec60c4b42be6 Mon Sep 17 00:00:00 2001 From: Shane Brunson Date: Mon, 12 Feb 2024 16:39:48 -0600 Subject: [PATCH] feat: add metric dimesion payload builder --- .gitignore | 1 + package.json | 4 +- src/logger.js | 18 +++++++ src/logger.test.js | 126 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 134 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 59ec266..01365e2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ xunit.xml flow-typed .DS_Store package-lock.json +.vscode/ diff --git a/package.json b/package.json index 51b9cef..8fff931 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "webpack": "babel-node --config-file ./node_modules/@krakenjs/grumbler-scripts/config/.babelrc-node --plugins=transform-es2015-modules-commonjs ./node_modules/.bin/webpack --progress", "format": "prettier --write --ignore-unknown .", "format:check": "prettier --check .", - "test:unit": "vitest run --coverage", - "test": "npm run format:check && npm run lint && npm run flow-typed && npm run flow && npm run test:unit", + "vitest": "vitest", + "test": "npm run format:check && npm run lint && npm run flow-typed && npm run flow && npm run vitest -- run --coverage", "build": "npm run test && npm run babel && npm run webpack", "clean": "rimraf dist coverage", "reinstall": "rimraf flow-typed && rimraf node_modules && npm install && flow-typed install", diff --git a/src/logger.js b/src/logger.js index ed0c310..76b5a59 100644 --- a/src/logger.js +++ b/src/logger.js @@ -57,6 +57,7 @@ export type LoggerType = {| addPayloadBuilder: AddBuilder, addMetaBuilder: AddBuilder, addTrackingBuilder: AddBuilder, + addMetricDimensionBuilder: AddBuilder, addHeaderBuilder: AddBuilder, setTransport: (Transport) => LoggerType, @@ -84,6 +85,7 @@ export function Logger({ const payloadBuilders: Array = []; const metaBuilders: Array = []; const trackingBuilders: Array = []; + const metricDimensionBuilders: Array = []; const headerBuilders: Array = []; function print( @@ -228,6 +230,10 @@ export function Logger({ return addBuilder(trackingBuilders, builder); } + function addMetricDimensionBuilder(builder): LoggerType { + return addBuilder(metricDimensionBuilders, builder); + } + function addHeaderBuilder(builder): LoggerType { return addBuilder(headerBuilders, builder); } @@ -270,6 +276,17 @@ export function Logger({ return logger; // eslint-disable-line no-use-before-define } + if (metricDimensionBuilders.length > 0 && !metricPayload.dimensions) { + metricPayload.dimensions = {}; + } + + for (const builder of metricDimensionBuilders) { + extendIfDefined( + metricPayload.dimensions || {}, + builder(metricPayload.dimensions || {}) + ); + } + print( LOG_LEVEL.DEBUG, `metric.${metricPayload.metricNamespace}`, @@ -343,6 +360,7 @@ export function Logger({ immediateFlush, addPayloadBuilder, addMetaBuilder, + addMetricDimensionBuilder, addTrackingBuilder, addHeaderBuilder, setTransport, diff --git a/src/logger.test.js b/src/logger.test.js index 50bbdf0..2684c9f 100644 --- a/src/logger.test.js +++ b/src/logger.test.js @@ -1,6 +1,7 @@ -/* eslint-disable flowtype/require-valid-file-annotation, eslint-comments/disable-enable-pair */ +/* @flow */ +/* eslint-disable eslint-comments/disable-enable-pair */ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { Logger } from "."; @@ -15,6 +16,19 @@ vi.stubGlobal("XMLHttpRequest", XMLHttpRequestMock); let logger; let logBuf; +const initLogger = ({ url = "/test/api/log" } = {}) => + Logger({ + url, + }); +const getLoggerBuffer = (testLogger) => testLogger.__buffer__; + +const validMetricPayload = { + metricNamespace: "namespace", + metricEventName: "event", + metricType: "counter", + metricValue: 1, +}; + describe("beaver exposes the Logger() function that returns a logger instance with the following methods... ", () => { beforeEach(() => { // create a new logger with a new buffer @@ -29,7 +43,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logger.__buffer__.tracking).toHaveLength(0); }); - it(".info(evtName, payload?) sends an info event obj to the events[] buffer ready for logging", () => { + test(".info(evtName, payload?) sends an info event obj to the events[] buffer ready for logging", () => { const expected = { event: "testing", level: "info", @@ -57,7 +71,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logBuf.events[1]).toMatchObject(expectedWithPayload); }); - it(".warn(evtName, payload?) sends a warn event obj to the events[] buffer ready for logging", () => { + test(".warn(evtName, payload?) sends a warn event obj to the events[] buffer ready for logging", () => { const expected = { event: "testing", level: "warn", @@ -83,7 +97,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logBuf.events[1]).toMatchObject(expectedWithPayload); }); - it(".error(evtName, payload?) sends an error event obj to the events[] buffer ready for logging", () => { + test(".error(evtName, payload?) sends an error event obj to the events[] buffer ready for logging", () => { const expected = { event: "house is on fire!!!", level: "error", @@ -109,7 +123,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logBuf.events[1]).toMatchObject(expectedWithPayload); }); - it(".debug(evtName, payload?) sends a debug event obj to the events[] buffer ready for logging", () => { + test(".debug(evtName, payload?) sends a debug event obj to the events[] buffer ready for logging", () => { const expected = { event: "gollum_personality_count", level: "debug", @@ -135,7 +149,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logBuf.events[1]).toMatchObject(expectedWithPayload); }); - it(".track(payload) sends general tracking info to the tracking[] buffer and will be sent with next flush", () => { + test(".track(payload) sends general tracking info to the tracking[] buffer and will be sent with next flush", () => { const expected = { token: "12345", country: "FR", @@ -151,9 +165,10 @@ describe("beaver exposes the Logger() function that returns a logger instance wi expect(logBuf.tracking[0]).toMatchObject(expected); }); - it(".metric(payload) sends general tracking info to the metrics[] buffer and will be sent with next flush", () => { + test(".metric(payload) sends general tracking info to the metrics[] buffer and will be sent with next flush", () => { const expected = { - name: "pp.xo.ui.lite.weasley.fallback-error", + metricNamespace: "pp.xo.ui.lite.weasley.fallback-error", + metricEventName: "event", metricValue: 5, dimensions: { country: "FR", @@ -162,6 +177,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi }; // 1) Won't error on empty / missing payload props + // $FlowIssue invalid metric payload logger.metric({}); expect(logBuf.metrics).toHaveLength(1); expect(logBuf.metrics[0]).toMatchObject({}); @@ -174,7 +190,7 @@ describe("beaver exposes the Logger() function that returns a logger instance wi }); describe("a beaver-logger instance exposes logger.__buffer__={} prop as a readonly object that...", () => { - it("doesn't allow its immedate children props to be reassigned", () => { + test("doesn't allow its immedate children props to be reassigned", () => { const loggerInstance = Logger({ url: "/test/api/log", }); @@ -187,17 +203,22 @@ describe("a beaver-logger instance exposes logger.__buffer__={} prop as a readon }); // verify we disallow re-assigning __buffer__ or its arrays + // $FlowIssue Invalid buffer expect(() => (loggerInstance.__buffer__ = {})).toThrow( "Cannot assign to read only property '__buffer__' of object '#'" ); + // $FlowIssue Invalid buffer expect(() => (loggerInstance.__buffer__.events = [])).toThrow( "Cannot set property events of # which has only a getter" ); // but using the buffer array methods directly is still allowed + // $FlowIssue Invalid buffer loggerInstance.__buffer__.metrics.push(1, 2, 3); + // $FlowIssue Invalid buffer loggerInstance.__buffer__.events.push(2); + // $FlowIssue Invalid buffer loggerInstance.__buffer__.tracking.push(3); expect(loggerInstance.__buffer__).toStrictEqual({ @@ -223,7 +244,7 @@ describe("beaver logger provides flushing methods that send events to the server expect(logBuf.metrics).toHaveLength(0); }); - it(".flush() clears the buffers (after sending data to server))", async () => { + test(".flush() clears the buffers (after sending data to server))", async () => { logger.info("testing"); expect(logBuf.events).toHaveLength(1); @@ -231,11 +252,11 @@ describe("beaver logger provides flushing methods that send events to the server expect(logBuf.events).toHaveLength(0); }); - // it(".flush() debounces then calls immediateFlush()) ", async () => { + // test(".flush() debounces then calls immediateFlush()) ", async () => { // TODO: need to test the promiseDebounce. Was unable to figure out how to do that. (zalgo...) // }); - it(".immediateflush() clears the buffers (after sending data to server))", async () => { + test(".immediateflush() clears the buffers (after sending data to server))", async () => { logger.info("testing"); expect(logBuf.events).toHaveLength(1); @@ -244,6 +265,85 @@ describe("beaver logger provides flushing methods that send events to the server }); }); +describe("addMetricDimensionBuilder", () => { + test("should add dimensions from builder", () => { + const testLogger = initLogger(); + + testLogger.addMetricDimensionBuilder(() => ({ + dimension1: "1", + dimension2: "2", + dimension3: "3", + })); + + testLogger.metric(validMetricPayload); + + expect(getLoggerBuffer(testLogger).metrics[0]).toEqual( + expect.objectContaining({ + dimensions: { + dimension1: "1", + dimension2: "2", + dimension3: "3", + }, + }) + ); + }); + + test("should overwrite existing dimensions", () => { + const testLogger = initLogger(); + + testLogger.addMetricDimensionBuilder(() => ({ + dimension1: "overwrite1", + dimension2: "overwrite2", + dimension3: "overwrite3", + })); + + testLogger.metric({ + ...validMetricPayload, + dimensions: { + dimension1: "1", + dimension2: "2", + dimension3: "3", + }, + }); + + expect(getLoggerBuffer(testLogger).metrics[0]).toEqual( + expect.objectContaining({ + dimensions: { + dimension1: "overwrite1", + dimension2: "overwrite2", + dimension3: "overwrite3", + }, + }) + ); + }); + + test("should merge dimensions", () => { + const testLogger = initLogger(); + + testLogger.addMetricDimensionBuilder(() => ({ + dimension1: "1", + dimension3: "3", + })); + + testLogger.metric({ + ...validMetricPayload, + dimensions: { + dimension2: "2", + }, + }); + + expect(getLoggerBuffer(testLogger).metrics[0]).toEqual( + expect.objectContaining({ + dimensions: { + dimension1: "1", + dimension2: "2", + dimension3: "3", + }, + }) + ); + }); +}); + /* TODO: Need to write unit tests for these exposed methods: addPayloadBuilder,