From fc822e30f5815cd90a9a910d9e51bfefa93ac87c Mon Sep 17 00:00:00 2001 From: Ludek Novy <13610612+ludeknovy@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:20:24 +0200 Subject: [PATCH] Instance id (#342) --- migrations/1694620030174_global-settings.js | 1 - migrations/1725550538880_global.js | 11 ++ migrations/1725551629571_instance-id.js | 7 + src/app.ts | 164 +++++++++--------- .../utils/analytics/analytics-event.spec.ts | 41 +++-- src/server/utils/analytics/anyltics-event.ts | 41 ++++- 6 files changed, 156 insertions(+), 109 deletions(-) create mode 100644 migrations/1725550538880_global.js create mode 100644 migrations/1725551629571_instance-id.js diff --git a/migrations/1694620030174_global-settings.js b/migrations/1694620030174_global-settings.js index 62ce39f2..69e7d875 100644 --- a/migrations/1694620030174_global-settings.js +++ b/migrations/1694620030174_global-settings.js @@ -12,4 +12,3 @@ exports.up = pgm => { }) } - diff --git a/migrations/1725550538880_global.js b/migrations/1725550538880_global.js new file mode 100644 index 00000000..b673d618 --- /dev/null +++ b/migrations/1725550538880_global.js @@ -0,0 +1,11 @@ +/* eslint-disable camelcase */ +const { PgLiteral } = require("node-pg-migrate") +exports.up = pgm => { + pgm.createTable({ schema: "jtl", name: "global" }, { + instance: { + type: "uuid", + "default": new PgLiteral("uuid_generate_v4()"), + notNull: true, + }, + }) +} diff --git a/migrations/1725551629571_instance-id.js b/migrations/1725551629571_instance-id.js new file mode 100644 index 00000000..4cc88b8e --- /dev/null +++ b/migrations/1725551629571_instance-id.js @@ -0,0 +1,7 @@ + +exports.up = async pgm => { + await pgm.db.query({ + text: `INSERT INTO jtl.global DEFAULT VALUES;`, + values: [], + }) +} diff --git a/src/app.ts b/src/app.ts index 0534fb14..527eb16b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,96 +21,96 @@ const DEFAULT_PORT = 5000 const PORT = process.env.PORT || DEFAULT_PORT export class App { - app: express.Application - router: Router = new Router() - private server: http.Server + app: express.Application + router: Router = new Router() + private server: http.Server - constructor() { - this.app = express() - this.config() - this.router.getRoutes(this.app) - this.databaseErrorHandler() - this.errorHandler() - } + constructor() { + this.app = express() + this.config() + this.router.getRoutes(this.app) + this.databaseErrorHandler() + this.errorHandler() + } - private config(): void { - this.app.use(bodyParser.json()) - this.app.use(bodyParser.urlencoded({ extended: false })) - this.app.use(compression()) - this.app.use(expressWinston.logger({ - transports: [ - new winston.transports.Console(), - ], - meta: false, - expressFormat: true, - colorize: false, - })) - this.app.use(helmet()) + private config(): void { + this.app.use(bodyParser.json()) + this.app.use(bodyParser.urlencoded({ extended: false })) + this.app.use(compression()) + this.app.use(expressWinston.logger({ + transports: [ + new winston.transports.Console(), + ], + meta: false, + expressFormat: true, + colorize: false, + })) + this.app.use(helmet()) - this.app.use((req, res, next) => { - res.header("Access-Control-Allow-Origin", "*") - res.header("Access-Control-Allow-Methods", "*") - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, x-access-token, Content-Type, Accept") - next() - }) - } + this.app.use((req, res, next) => { + res.header("Access-Control-Allow-Origin", "*") + res.header("Access-Control-Allow-Methods", "*") + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, x-access-token, Content-Type, Accept") + next() + }) + } - private errorHandler() { - // eslint-disable-next-line no-unused-vars - this.app.use(function (error: Error, req: Request, res: Response, next: NextFunction) { - if (boom.isBoom(error)) { - const { payload: { message } } = error.output - return res.status(error.output.statusCode).json({ message }) - } - const errorId = uuidv4() - logger.error(`Unexpected error: ${error}, errorId: ${errorId}`) - AnalyticsEvent.reportUnexpectedError(error) - return res.status(StatusCode.InternalError).json({ message: `Unexpected error occurred: ${errorId}` }) + private errorHandler() { + // eslint-disable-next-line no-unused-vars + this.app.use(function (error: Error, req: Request, res: Response, next: NextFunction) { + if (boom.isBoom(error)) { + const { payload: { message } } = error.output + return res.status(error.output.statusCode).json({ message }) + } + const errorId = uuidv4() + logger.error(`Unexpected error: ${error}, errorId: ${errorId}`) + AnalyticsEvent.reportUnexpectedError(error) + return res.status(StatusCode.InternalError).json({ message: `Unexpected error occurred: ${errorId}` }) - }) - } + }) + } - private databaseErrorHandler() { - this.app.use(function (error: PgError, req: Request, res: Response, next: NextFunction) { - logger.error(error) - if (error instanceof pgp.errors.QueryResultError) { - return next(boom.notFound()) - } - if (error?.code === "ECONNREFUSED") { - return next(boom.serverUnavailable(`Could not connect to the database: ${error.address}:${error.port}`)) - } - return next(error) + private databaseErrorHandler() { + this.app.use(function (error: PgError, req: Request, res: Response, next: NextFunction) { + logger.error(error) + if (error instanceof pgp.errors.QueryResultError) { + return next(boom.notFound()) + } + if (error?.code === "ECONNREFUSED") { + return next(boom.serverUnavailable(`Could not connect to the database: ${error.address}:${error.port}`)) + } + return next(error) - }) - } + }) + } - listen() { - if (!config.jwtToken || !config.jwtTokenLogin) { - logger.error("Please provide JWT_TOKEN and JWT_TOKEN_LOGIN env vars") - process.exit(1) + listen() { + if (!config.jwtToken || !config.jwtTokenLogin) { + logger.error("Please provide JWT_TOKEN and JWT_TOKEN_LOGIN env vars") + process.exit(1) + } + this.server = this.app.listen(PORT, + () => { + logger.info("Express server listening on port " + PORT) + bree.start().then(() => { + + logger.info("Bree scheduler was started") + if (process.env.OPT_OUT_ANALYTICS === "true") { + bree.stop("analytics-report").then(() => { + logger.info("Analytics task was opted-out") + }) + } else { + logger.info("By using this app you agree with the use of analytics" + + " in this app to help improve user experience and the overall functionality of the app.") + } + }) + }) + return this.server } - this.server = this.app.listen(PORT, - () => { - logger.info("Express server listening on port " + PORT) - bree.start().then(() => { - process.env.ANALYTICS_IDENTIFIER = uuidv4() - logger.info("Bree scheduler was started") - if (process.env.OPT_OUT_ANALYTICS === "true") { - bree.stop("analytics-report").then(() => { - logger.info("Analytics task was opted-out") - }) - } else { - logger.info("By using this app you agree with the use of analytics in this app to help improve" + - " user experience and the overall functionality of the app.") - } - }) - }) - return this.server - } - close() { - return this.server.close(() => { - logger.info("Server closed") - }) - } + close() { + return this.server.close(() => { + logger.info("Server closed") + }) + } } diff --git a/src/server/utils/analytics/analytics-event.spec.ts b/src/server/utils/analytics/analytics-event.spec.ts index 35258679..793e2f8c 100644 --- a/src/server/utils/analytics/analytics-event.spec.ts +++ b/src/server/utils/analytics/analytics-event.spec.ts @@ -3,7 +3,6 @@ import { analytics } from "../analytics" jest.mock("../analytics") - describe("AnalyticEvents", () => { beforeEach(() => { @@ -29,66 +28,72 @@ describe("AnalyticEvents", () => { }) }) describe("reportProcessingFinished", () => { - it("should not track the event when analytics disabled", function () { + it("should not track the event when analytics disabled", async function () { process.env.OPT_OUT_ANALYTICS = "true" const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportProcessingFinished() + await AnalyticsEvent.reportProcessingFinished() expect(trackMock).not.toHaveBeenCalled() }) - it("should track the event only when analytics enabled", function () { + it("should track the event only when analytics enabled", async function () { process.env.OPT_OUT_ANALYTICS = "false" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportProcessingFinished() + await AnalyticsEvent.reportProcessingFinished() expect(trackMock).toHaveBeenCalled() }) }) describe("reportLabelCount", () => { - it("should not track the event when analytics disabled", function () { + it("should not track the event when analytics disabled", async function () { process.env.OPT_OUT_ANALYTICS = "true" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportDetails(1, 1) + await AnalyticsEvent.reportDetails(1, 1) expect(trackMock).not.toHaveBeenCalled() }) - it("should track the event only when analytics enabled", function () { + it("should track the event only when analytics enabled", async function () { process.env.OPT_OUT_ANALYTICS = "false" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportDetails(1, 1) + await AnalyticsEvent.reportDetails(1, 1) expect(trackMock).toHaveBeenCalled() }) }) describe("reportProcessingStarted", () => { - it("should not track the event when analytics disabled", function () { + it("should not track the event when analytics disabled", async function () { process.env.OPT_OUT_ANALYTICS = "true" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportProcessingStarted() + await AnalyticsEvent.reportProcessingStarted() expect(trackMock).not.toHaveBeenCalled() }) - it("should track the event only when analytics enabled", function () { + it("should track the event only when analytics enabled", async function () { process.env.OPT_OUT_ANALYTICS = "false" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportProcessingStarted() + await AnalyticsEvent.reportProcessingStarted() expect(trackMock).toHaveBeenCalled() }) }) describe("unexpectedError", () => { - it("should not track the event when analytics disabled", function () { + it("should not track the event when analytics disabled", async function () { process.env.OPT_OUT_ANALYTICS = "true" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportUnexpectedError(Error("test")) + await AnalyticsEvent.reportUnexpectedError(Error("test")) expect(trackMock).not.toHaveBeenCalled() }) - it("should track the event only when analytics enabled", function () { + it("should track the event only when analytics enabled", async function () { process.env.OPT_OUT_ANALYTICS = "false" + jest.spyOn(AnalyticsEvent as any, "getInstanceId").mockResolvedValueOnce("mocked-id") const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) - AnalyticsEvent.reportUnexpectedError(Error("test")) + await AnalyticsEvent.reportUnexpectedError(Error("test")) expect(trackMock).toHaveBeenCalled() }) }) - }) diff --git a/src/server/utils/analytics/anyltics-event.ts b/src/server/utils/analytics/anyltics-event.ts index 91dbf666..3fc4e563 100644 --- a/src/server/utils/analytics/anyltics-event.ts +++ b/src/server/utils/analytics/anyltics-event.ts @@ -1,44 +1,69 @@ import { analytics } from "../analytics" +import { db } from "../../../db/db" +import { logger } from "../../../logger" +import { v4 as uuidv4 } from "uuid" + +let INSTANCE_ID = null +const FALLBACK_ID = uuidv4() export class AnalyticsEvent { + private static async getInstanceId(): Promise { + if (INSTANCE_ID !== null) { + return INSTANCE_ID + } + try { + const result = await db.oneOrNone("SELECT instance FROM jtl.global") + if (result && result.instance) { + INSTANCE_ID = result.instance + return INSTANCE_ID + } + return FALLBACK_ID + + } catch(error) { + logger.info("Instance id could not be loaded " + error) + return FALLBACK_ID + } + + } + static isAnalyticEnabled(): boolean { return !(process.env.OPT_OUT_ANALYTICS === "true") } - static reportProcessingStarted() { + static async reportProcessingStarted() { if (this.isAnalyticEnabled()) { analytics.track("reportProcessingStarted", { // eslint-disable-next-line camelcase - distinct_id: process.env.ANALYTICS_IDENTIFIER, + distinct_id: await this.getInstanceId(), }) } } - static reportProcessingFinished() { + static async reportProcessingFinished() { if (this.isAnalyticEnabled()) { analytics.track("reportProcessingFinished", { // eslint-disable-next-line camelcase - distinct_id: process.env.ANALYTICS_IDENTIFIER, + distinct_id: await this.getInstanceId(), }) } } - static reportDetails(labelCount, duration) { + static async reportDetails(labelCount, duration) { if (this.isAnalyticEnabled()) { analytics.track("reportInformation", { // eslint-disable-next-line camelcase - distinct_id: process.env.ANALYTICS_IDENTIFIER, + distinct_id: await this.getInstanceId(), labelCount, duration, }) } } - static reportUnexpectedError(error) { + static async reportUnexpectedError(error) { if (this.isAnalyticEnabled()) { analytics.track("unexpectedError", { - distinct_id: process.env.ANALYTICS_IDENTIFIER, + distinct_id: await this.getInstanceId(), error, }) }