From 7e9e48e63136ed6da11ef9b45c002308643d7002 Mon Sep 17 00:00:00 2001 From: Iku-turso Date: Wed, 31 Aug 2022 14:43:12 +0300 Subject: [PATCH] Implement a decorator to emit telemetry for any specified function-injectable Signed-off-by: Iku-turso --- .../app-event-bus/emit-event.injectable.ts | 1 + ...y-from-white-listed-function-calls.test.ts | 116 ++++++++++++++++++ .../renderer/emit-telemetry.injectable.ts | 27 ++++ .../telemetry-decorator.injectable.ts | 65 ++++++++++ ...try-white-list-for-functions.injectable.ts | 13 ++ 5 files changed, 222 insertions(+) create mode 100644 src/features/telemetry/emit-telemetry-from-white-listed-function-calls.test.ts create mode 100644 src/features/telemetry/renderer/emit-telemetry.injectable.ts create mode 100644 src/features/telemetry/renderer/telemetry-decorator.injectable.ts create mode 100644 src/features/telemetry/renderer/telemetry-white-list-for-functions.injectable.ts diff --git a/src/common/app-event-bus/emit-event.injectable.ts b/src/common/app-event-bus/emit-event.injectable.ts index 47bf2ca691..d5aaafe37b 100644 --- a/src/common/app-event-bus/emit-event.injectable.ts +++ b/src/common/app-event-bus/emit-event.injectable.ts @@ -8,6 +8,7 @@ import appEventBusInjectable from "./app-event-bus.injectable"; const emitEventInjectable = getInjectable({ id: "emit-event", instantiate: (di) => di.inject(appEventBusInjectable).emit, + decorable: false, }); export default emitEventInjectable; diff --git a/src/features/telemetry/emit-telemetry-from-white-listed-function-calls.test.ts b/src/features/telemetry/emit-telemetry-from-white-listed-function-calls.test.ts new file mode 100644 index 0000000000..97c68c8ad0 --- /dev/null +++ b/src/features/telemetry/emit-telemetry-from-white-listed-function-calls.test.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; +import telemetryWhiteListForFunctionsInjectable from "./renderer/telemetry-white-list-for-functions.injectable"; +import { runInAction } from "mobx"; +import emitEventInjectable from "../../common/app-event-bus/emit-event.injectable"; + +describe("sending-telemetry-from-white-listed-function-calls", () => { + let di: DiContainer; + + beforeEach(() => { + di = getDiForUnitTesting({ doGeneralOverrides: true }); + }); + + describe("given a telemetry white-list for injectables which instantiate a function", () => { + let emitEventMock: jest.Mock; + + beforeEach(() => { + di.override(telemetryWhiteListForFunctionsInjectable, () => [ + "some-white-listed-function", + ]); + + emitEventMock = jest.fn(); + di.override(emitEventInjectable, () => emitEventMock); + }); + + describe("given instances of white-listed, non-white-listed and tagged functions", () => { + let whiteListedFunctionMock: jest.Mock; + let nonWhiteListedFunctionMock: jest.Mock; + let taggedFunctionMock: jest.Mock; + let injectedWhiteListedFunction: jest.Mock; + let injectedNonWhiteListedFunction: jest.Mock; + let injectedTaggedFunction: jest.Mock; + + beforeEach(() => { + whiteListedFunctionMock = jest.fn(); + nonWhiteListedFunctionMock = jest.fn(); + taggedFunctionMock = jest.fn(); + + const whiteListedInjectable = getInjectable({ + id: "some-white-listed-function", + instantiate: () => whiteListedFunctionMock, + }); + + const nonWhiteListedInjectable = getInjectable({ + id: "some-non-white-listed-function", + instantiate: () => nonWhiteListedFunctionMock, + }); + + const taggedInjectable = getInjectable({ + id: "some-tagged-function", + instantiate: () => taggedFunctionMock, + tags: ["emit-telemetry"], + }); + + runInAction(() => { + di.register(whiteListedInjectable); + di.register(nonWhiteListedInjectable); + di.register(taggedInjectable); + }); + + injectedWhiteListedFunction = di.inject(whiteListedInjectable); + injectedNonWhiteListedFunction = di.inject(nonWhiteListedInjectable); + injectedTaggedFunction = di.inject(taggedInjectable); + }); + + it("telemetry is not emitted yet", () => { + expect(emitEventMock).not.toHaveBeenCalled(); + }); + + describe("when the white-listed function is called", () => { + beforeEach(() => { + injectedWhiteListedFunction("some-arg", "some-other-arg"); + }); + + it("telemetry is emitted in event bus", () => { + expect(emitEventMock).toHaveBeenCalledWith({ + destination: "auto-capture", + action: "telemetry-from-business-action", + name: "some-white-listed-function", + params: { args: ["some-arg", "some-other-arg"] }, + }); + }); + }); + + describe("when the non-white-listed function is called", () => { + beforeEach(() => { + injectedNonWhiteListedFunction(); + }); + + it("telemetry is not emitted", () => { + expect(emitEventMock).not.toHaveBeenCalled(); + }); + }); + + describe("when the tagged, but not white-listed function is called", () => { + beforeEach(() => { + injectedTaggedFunction("some-arg", "some-other-arg"); + }); + + it("telemetry is emitted in event bus", () => { + expect(emitEventMock).toHaveBeenCalledWith({ + destination: "auto-capture", + action: "telemetry-from-business-action", + name: "some-tagged-function", + params: { args: ["some-arg", "some-other-arg"] }, + }); + }); + }); + }); + }); +}); diff --git a/src/features/telemetry/renderer/emit-telemetry.injectable.ts b/src/features/telemetry/renderer/emit-telemetry.injectable.ts new file mode 100644 index 0000000000..149bdb79bb --- /dev/null +++ b/src/features/telemetry/renderer/emit-telemetry.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import emitEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; + +const emitTelemetryInjectable = getInjectable({ + id: "emit-telemetry", + + instantiate: (di) => { + const emitEvent = di.inject(emitEventInjectable); + + return ({ action, args }: { action: string; args: any[] }) => { + emitEvent({ + destination: "auto-capture", + action: "telemetry-from-business-action", + name: action, + params: { args }, + }); + }; + }, + + decorable: false, +}); + +export default emitTelemetryInjectable; diff --git a/src/features/telemetry/renderer/telemetry-decorator.injectable.ts b/src/features/telemetry/renderer/telemetry-decorator.injectable.ts new file mode 100644 index 0000000000..1dc4071571 --- /dev/null +++ b/src/features/telemetry/renderer/telemetry-decorator.injectable.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainerForInjection } from "@ogre-tools/injectable"; + +import { + lifecycleEnum, + getInjectable, + instantiationDecoratorToken, +} from "@ogre-tools/injectable"; +import assert from "assert"; + + +import { isFunction } from "lodash/fp"; +import emitTelemetryInjectable from "./emit-telemetry.injectable"; +import telemetryWhiteListForFunctionsInjectable from "./telemetry-white-list-for-functions.injectable"; + +const telemetryDecoratorInjectable = getInjectable({ + id: "telemetry-decorator", + + instantiate: (diForDecorator) => { + const emitTelemetry = diForDecorator.inject(emitTelemetryInjectable); + + const whiteList = diForDecorator.inject( + telemetryWhiteListForFunctionsInjectable, + ); + + return { + decorate: + (instantiateToBeDecorated: any) => + (di: DiContainerForInjection, instantiationParameter: any) => { + const instance = instantiateToBeDecorated(di, instantiationParameter); + + if (isFunction(instance)) { + return (...args: any[]) => { + const currentContext = di.context.at(-1); + + assert(currentContext); + + if (shouldEmitTelemetry(currentContext, whiteList)) { + emitTelemetry({ action: currentContext.injectable.id, args }); + } + + return instance(...args); + }; + } + + return instance; + }, + }; + }, + + decorable: false, + // Todo: this is required because of imperfect typing in injectable. + lifecycle: lifecycleEnum.singleton, + injectionToken: instantiationDecoratorToken, +}); + +const shouldEmitTelemetry = (currentContext: any, whiteList: any) => ( + currentContext.injectable.tags?.includes("emit-telemetry") || + whiteList.includes(currentContext.injectable.id) +); + +export default telemetryDecoratorInjectable; diff --git a/src/features/telemetry/renderer/telemetry-white-list-for-functions.injectable.ts b/src/features/telemetry/renderer/telemetry-white-list-for-functions.injectable.ts new file mode 100644 index 0000000000..3c7ac58336 --- /dev/null +++ b/src/features/telemetry/renderer/telemetry-white-list-for-functions.injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const telemetryWhiteListForFunctionsInjectable = getInjectable({ + id: "telemetry-white-list-for-functions", + instantiate: () => ["some-placeholder-injectable-id"], + decorable: false, +}); + +export default telemetryWhiteListForFunctionsInjectable;