Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance id #342

Merged
merged 9 commits into from
Sep 27, 2024
1 change: 0 additions & 1 deletion migrations/1694620030174_global-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ exports.up = pgm => {
})

}

11 changes: 11 additions & 0 deletions migrations/1725550538880_global.js
Original file line number Diff line number Diff line change
@@ -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,
},
})
}
7 changes: 7 additions & 0 deletions migrations/1725551629571_instance-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

exports.up = async pgm => {
await pgm.db.query({
text: `INSERT INTO jtl.global DEFAULT VALUES;`,
values: [],
})
}
164 changes: 82 additions & 82 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
}
41 changes: 23 additions & 18 deletions src/server/utils/analytics/analytics-event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { analytics } from "../analytics"

jest.mock("../analytics")


describe("AnalyticEvents", () => {

beforeEach(() => {
Expand All @@ -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()
})
})

})
41 changes: 33 additions & 8 deletions src/server/utils/analytics/anyltics-event.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
if (INSTANCE_ID !== null) {
return INSTANCE_ID

Check warning on line 13 in src/server/utils/analytics/anyltics-event.ts

View check run for this annotation

Codecov / codecov/patch

src/server/utils/analytics/anyltics-event.ts#L13

Added line #L13 was not covered by tests
}
try {
const result = await db.oneOrNone("SELECT instance FROM jtl.global")

Check warning on line 16 in src/server/utils/analytics/anyltics-event.ts

View check run for this annotation

Codecov / codecov/patch

src/server/utils/analytics/anyltics-event.ts#L15-L16

Added lines #L15 - L16 were not covered by tests
if (result && result.instance) {
INSTANCE_ID = result.instance
return INSTANCE_ID

Check warning on line 19 in src/server/utils/analytics/anyltics-event.ts

View check run for this annotation

Codecov / codecov/patch

src/server/utils/analytics/anyltics-event.ts#L18-L19

Added lines #L18 - L19 were not covered by tests
}
return FALLBACK_ID

Check warning on line 21 in src/server/utils/analytics/anyltics-event.ts

View check run for this annotation

Codecov / codecov/patch

src/server/utils/analytics/anyltics-event.ts#L21

Added line #L21 was not covered by tests

} catch(error) {
logger.info("Instance id could not be loaded " + error)
return FALLBACK_ID

Check warning on line 25 in src/server/utils/analytics/anyltics-event.ts

View check run for this annotation

Codecov / codecov/patch

src/server/utils/analytics/anyltics-event.ts#L24-L25

Added lines #L24 - L25 were not covered by tests
}

}

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,
})
}
Expand Down
Loading