From 70c2373d0d305bdf9ef19bd6d7ec836422347fc0 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 17 Feb 2024 20:21:05 +0700 Subject: [PATCH] feat: instrument sentry (#57) --- Dockerfile | 2 +- build.js | 34 - package.json | 17 +- pnpm-lock.yaml | 658 ++++++++++++++++-- src/application/App.ts | 58 +- src/application/adapters/GithubAdapter.ts | 173 ++--- src/application/webhook/GithubWebhook.ts | 91 +-- src/index.ts | 20 +- src/presentation/TelegramPresenter.ts | 2 + src/presentation/event-handlers/Deployment.ts | 39 +- src/presentation/event-handlers/Discussion.ts | 183 ++--- src/presentation/routes/GithubRoute.ts | 38 +- src/presentation/routes/GitlabRoute.ts | 31 +- src/tracing.ts | 49 ++ src/utils/honoOtelTracer.ts | 53 ++ tsconfig.json | 6 +- 16 files changed, 1065 insertions(+), 389 deletions(-) delete mode 100644 build.js create mode 100644 src/tracing.ts create mode 100644 src/utils/honoOtelTracer.ts diff --git a/Dockerfile b/Dockerfile index 2246897..d5fe35d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ ENV PORT=3000 EXPOSE ${PORT} -CMD [ "node", "dist/index.js" ] +CMD [ "node", "--import", "./dist/src/tracing.js", "./dist/src/index.js" ] diff --git a/build.js b/build.js deleted file mode 100644 index 038abaa..0000000 --- a/build.js +++ /dev/null @@ -1,34 +0,0 @@ -import esbuild from "esbuild"; - -esbuild - .build({ - entryPoints: ["src/index.ts"], - sourcemap: "external", - bundle: true, - format: "esm", - platform: "node", - external: [ - "cheerio", - "colorette", - "eventsource", - "grammy", - "gura", - "polka", - "remark-gfm", - "remark-html", - "remark-parse", - "rxjs", - "sanitize-html", - "templite", - "unified", - "zod" - ], - outdir: "./dist", - target: ["node18.12"], - tsconfig: "tsconfig.json" - }) - .catch((e) => { - /* eslint-disable-next-line */ - console.error(e); - process.exit(1); - }); \ No newline at end of file diff --git a/package.json b/package.json index 93484dc..c281008 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "dist/index.js", "type": "module", "scripts": { - "dev:start": "node --experimental-loader @esbuild-kit/esm-loader --env-file .env src/index.ts", + "dev:start": "node --experimental-loader @esbuild-kit/esm-loader --env-file .env -r src/tracing.ts src/index.ts", "dev": "nodemon -e ts --watch src --exec \"npm run dev:start\"", - "build": "tsc --noEmit && node build.js", + "build": "tsc && tsc-alias", "test:unit": "vitest run", "test:coverage": "vitest run --coverage", "test:tdd": "vitest", @@ -40,19 +40,30 @@ "eslint-plugin-import": "^2.29.1", "husky": "^8.0.3", "nodemon": "^3.0.3", + "tsc-alias": "^1.8.8", "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.1.3", "vitest": "^1.3.0" }, "dependencies": { + "@hono/node-server": "^1.8.0", "@octokit/webhooks-types": "^7.3.2", + "@opentelemetry/api": "^1.7.0", + "@opentelemetry/context-async-hooks": "^1.21.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.48.0", + "@opentelemetry/instrumentation-http": "^0.48.0", + "@opentelemetry/sdk-metrics": "^1.21.0", + "@opentelemetry/sdk-node": "^0.48.0", + "@opentelemetry/sdk-trace-node": "^1.21.0", + "@sentry/node": "^7.101.1", + "@sentry/opentelemetry": "^7.101.1", "cheerio": "1.0.0-rc.12", "colorette": "^2.0.20", "eventsource": "^2.0.2", "grammy": "^1.21.1", "gura": "^1.4.4", - "polka": "1.0.0-next.22", + "hono": "^4.0.3", "remark-gfm": "^4.0.0", "remark-html": "^16.0.1", "remark-parse": "^11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 079b33e..d905893 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,39 @@ settings: excludeLinksFromLockfile: false dependencies: + '@hono/node-server': + specifier: ^1.8.0 + version: 1.8.0 '@octokit/webhooks-types': specifier: ^7.3.2 version: 7.3.2 + '@opentelemetry/api': + specifier: ^1.7.0 + version: 1.7.0 + '@opentelemetry/context-async-hooks': + specifier: ^1.21.0 + version: 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-trace-otlp-grpc': + specifier: ^0.48.0 + version: 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/instrumentation-http': + specifier: ^0.48.0 + version: 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': + specifier: ^1.21.0 + version: 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-node': + specifier: ^0.48.0 + version: 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.21.0 + version: 1.21.0(@opentelemetry/api@1.7.0) + '@sentry/node': + specifier: ^7.101.1 + version: 7.101.1 + '@sentry/opentelemetry': + specifier: ^7.101.1 + version: 7.101.1(@opentelemetry/api@1.7.0)(@opentelemetry/core@1.21.0)(@opentelemetry/sdk-trace-base@1.21.0)(@opentelemetry/semantic-conventions@1.21.0) cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 @@ -23,9 +53,9 @@ dependencies: gura: specifier: ^1.4.4 version: 1.4.4 - polka: - specifier: 1.0.0-next.22 - version: 1.0.0-next.22 + hono: + specifier: ^4.0.3 + version: 4.0.3 remark-gfm: specifier: ^4.0.0 version: 4.0.0 @@ -91,6 +121,9 @@ devDependencies: nodemon: specifier: ^3.0.3 version: 3.0.3 + tsc-alias: + specifier: ^1.8.8 + version: 1.8.8 tslib: specifier: ^2.6.2 version: 2.6.2 @@ -817,6 +850,30 @@ packages: resolution: {integrity: sha512-KZlJcOdgD8N6SMIHWfwmb11L5/L2+rptWBxCexbzNCry/wfLXFFGFniyUrySnvFo+cWMxgy5GuImwY3mhUrP8g==} dev: false + /@grpc/grpc-js@1.10.1: + resolution: {integrity: sha512-55ONqFytZExfOIjF1RjXPcVmT/jJqFzbbDqxK9jmRV4nxiYWtL9hENSW1Jfx0SdZfrvoqd44YJ/GJTqfRrawSQ==} + engines: {node: ^8.13.0 || >=10.10.0} + dependencies: + '@grpc/proto-loader': 0.7.10 + '@types/node': 20.11.19 + dev: false + + /@grpc/proto-loader@0.7.10: + resolution: {integrity: sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.2.6 + yargs: 17.7.2 + dev: false + + /@hono/node-server@1.8.0: + resolution: {integrity: sha512-vlebYBWAE9PVbRXps8O918Cj34F1FLCJW+ZlepcIpLKp0SRXgDZ2tpYbSoJeT/QOkKVP6TQcjfTl5rs3z88rVw==} + engines: {node: '>=18.14.1'} + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -908,8 +965,328 @@ packages: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: true - /@polka/url@1.0.0-next.21: - resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + /@opentelemetry/api-logs@0.48.0: + resolution: {integrity: sha512-1/aMiU4Eqo3Zzpfwu51uXssp5pzvHFObk8S9pKAiXb1ne8pvg1qxBQitYL1XUiAMEXFzgjaidYG2V6624DRhhw==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.7.0 + dev: false + + /@opentelemetry/api@1.7.0: + resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} + engines: {node: '>=8.0.0'} + dev: false + + /@opentelemetry/context-async-hooks@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-t0iulGPiMjG/NrSjinPQoIf8ST/o9V0dGOJthfrFporJlNdlKIQPfC7lkrV+5s2dyBThfmSbJlp/4hO1eOcDXA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + dev: false + + /@opentelemetry/core@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/semantic-conventions': 1.21.0 + dev: false + + /@opentelemetry/exporter-trace-otlp-grpc@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-+qRQXUbdRW6aNRT5yWOG3G6My1VxxKeqgUyLkkdIjkT20lvymjiN2RpBfGMtAf/oqnuRknf9snFl9VSIO2gniw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@grpc/grpc-js': 1.10.1 + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-http@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-QEZKbfWqXrbKVpr2PHd4KyKI0XVOhUYC+p2RPV8s+2K5QzZBE3+F9WlxxrXDfkrvGmpQAZytBoHQQYA3AGOtpw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/exporter-trace-otlp-proto@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-hVXr/8DYlAKAzQYMsCf3ZsGweS6NTK3IHIEqmLokJZYcvJQBEEazeAdISfrL/utWnapg1Qnpw8u+W6SpxNzmTw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-proto-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/exporter-zipkin@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-J0ejrOx52s1PqvjNalIHvY/4v9ZxR2r7XS7WZbwK3qpVYZlGVq5V1+iCNweqsKnb/miUt/4TFvJBc9f5Q/kGcA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + dev: false + + /@opentelemetry/instrumentation-http@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-uXqOsLhW9WC3ZlGm6+PSX0xjSDTCfy4CMjfYj6TPWusOO8dtdx040trOriF24y+sZmS3M+5UQc6/3/ZxBJh4Mw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/instrumentation': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/instrumentation@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-sjtZQB5PStIdCw5ovVTDGwnmQC+GGYArJNgIcydrDSqUTdYBnMrN9P4pwQZgS3vTGIp+TU1L8vMXGe51NVmIKQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@types/shimmer': 1.0.5 + import-in-the-middle: 1.7.1 + require-in-the-middle: 7.2.0 + semver: 7.5.4 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/otlp-exporter-base@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-T4LJND+Ugl87GUONoyoQzuV9qCn4BFIPOnCH1biYqdGhc2JahjuLqVD9aefwLzGBW638iLAo88Lh68h2F1FLiA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/otlp-grpc-exporter-base@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-Vdp56RK9OU+Oeoy3YQC/UMOWglKQ9qvgGr49FgF4r8vk5DlcTUgVS0m3KG8pykmRPA+5ZKaDuqwPw5aTvWmHFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@grpc/grpc-js': 1.10.1 + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + protobufjs: 7.2.6 + dev: false + + /@opentelemetry/otlp-proto-exporter-base@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-14GSTvPZPfrWsB54fYMGb8v+Uge5xGXyz0r2rf4SzcRnO2hXCPHEuL3yyL50emaKPAY+fj29Dm0bweawe8UA6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.48.0(@opentelemetry/api@1.7.0) + protobufjs: 7.2.6 + dev: false + + /@opentelemetry/otlp-transformer@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-yuoS4cUumaTK/hhxW3JUy3wl2U4keMo01cFDrUOmjloAdSSXvv1zyQ920IIH4lymp5Xd21Dj2/jq2LOro56TJg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.48.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-logs': 0.48.0(@opentelemetry/api-logs@0.48.0)(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/propagator-b3@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-3ZTobj2VDIOzLsIvvYCdpw6tunxUVElPxDvog9lS49YX4hohHeD84A8u9Ns/6UYUcaN5GSoEf891lzhcBFiOLA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/propagator-jaeger@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-8TQSwXjBmaDx7JkxRD7hdmBmRK2RGRgzHX1ArJfJhIc5trzlVweyorzqQrXOvqVEdEg+zxUMHkL5qbGH/HDTPA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/resources@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-1Z86FUxPKL6zWVy2LdhueEGl9AHDJcx+bvHStxomruz6Whd02mE3lNUMjVJ+FGRoktx/xYQcxccYb03DiUP6Yw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + dev: false + + /@opentelemetry/sdk-logs@0.48.0(@opentelemetry/api-logs@0.48.0)(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-lRcA5/qkSJuSh4ItWCddhdn/nNbVvnzM+cm9Fg1xpZUeTeozjJDBcHnmeKoOaWRnrGYBdz6UTY6bynZR9aBeAA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.8.0' + '@opentelemetry/api-logs': '>=0.39.1' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.48.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + dev: false + + /@opentelemetry/sdk-metrics@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-on1jTzIHc5DyWhRP+xpf+zrgrREXcHBH4EDAfaB5mIG7TWpKxNXooQ1JCylaPsswZUv4wGnVTinr4HrBdGARAQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + lodash.merge: 4.6.2 + dev: false + + /@opentelemetry/sdk-node@0.48.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-3o3GS6t+VLGVFCV5bqfGOcWIgOdkR/UE6Qz7hHksP5PXrVBeYsPqts7cPma5YXweaI3r3h26mydg9PqQIcqksg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.48.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-trace-otlp-http': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-zipkin': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/instrumentation': 0.48.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-logs': 0.48.0(@opentelemetry/api-logs@0.48.0)(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-node': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@opentelemetry/sdk-trace-base@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-yrElGX5Fv0umzp8Nxpta/XqU71+jCAyaLk34GmBzNcrW43nqbrqvdPs4gj4MVy/HcTjr6hifCDCYA3rMkajxxA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + dev: false + + /@opentelemetry/sdk-trace-node@1.21.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-1pdm8jnqs+LuJ0Bvx6sNL28EhC8Rv7NYV8rnoXq3GIQo7uOHBDAFSj7makAfbakrla7ecO1FRfI8emnR4WvhYA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.8.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/context-async-hooks': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/propagator-b3': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/propagator-jaeger': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + semver: 7.5.4 + dev: false + + /@opentelemetry/semantic-conventions@1.21.0: + resolution: {integrity: sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==} + engines: {node: '>=14'} + dev: false + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} dev: false /@rollup/rollup-android-arm-eabi@4.9.5: @@ -1016,6 +1393,63 @@ packages: dev: true optional: true + /@sentry-internal/tracing@7.101.1: + resolution: {integrity: sha512-ihjWG8x4x0ozx6t+EHoXLKbsPrgzYLCpeBLWyS+M6n3hn6cmHM76c8nZw3ldhUQi5UYL3LFC/JZ50b4oSxtlrg==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/core@7.101.1: + resolution: {integrity: sha512-XSmXXeYT1d4O14eDF3OXPJFUgaN2qYEeIGUztqPX9nBs9/ij8y/kZOayFqlIMnfGvjOUM+63sy/2xDBOpFn6ug==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/node@7.101.1: + resolution: {integrity: sha512-iXSxUT6Zbt/KUY0+fRcW5II6Tgp2zdTfhBW+fQuDt/UUZt7Ypvb+6n4U2oom3LJfttmD7mdjQuT4+vsNImDjTQ==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.101.1 + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/opentelemetry@7.101.1(@opentelemetry/api@1.7.0)(@opentelemetry/core@1.21.0)(@opentelemetry/sdk-trace-base@1.21.0)(@opentelemetry/semantic-conventions@1.21.0): + resolution: {integrity: sha512-v+5/WGm9M4ihiVCLPTflgXQ1p5iciRASg5EasE+onlaflaorwKln+BvGTzMHmu3DClMei0CHkoPPj7qqsIKDYA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/core': ^1.0.0 + '@opentelemetry/sdk-trace-base': ^1.0.0 + '@opentelemetry/semantic-conventions': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.21.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.21.0 + '@sentry/core': 7.101.1 + '@sentry/types': 7.101.1 + '@sentry/utils': 7.101.1 + dev: false + + /@sentry/types@7.101.1: + resolution: {integrity: sha512-bwtkQvrCZ6JGc7vqX7TEAKBgkbQFORt84FFS3JQQb8G3efTt9fZd2ReY4buteKQdlALl8h1QWVngTLmI+kyUuw==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@7.101.1: + resolution: {integrity: sha512-Nrg0nrEI3nrOCd9SLJ/WGzxS5KMQE4cryLOvrDcHJRWpsSyGBF1hLLerk84Nsw/0myMsn7zTYU+xoq7idNsX5A==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.101.1 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -1102,7 +1536,6 @@ packages: resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} dependencies: undici-types: 5.26.5 - dev: true /@types/sanitize-html@2.11.0: resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==} @@ -1114,6 +1547,10 @@ packages: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true + /@types/shimmer@1.0.5: + resolution: {integrity: sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==} + dev: false + /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: false @@ -1328,6 +1765,14 @@ packages: event-target-shim: 5.0.1 dev: false + /acorn-import-assertions@1.9.0(acorn@8.11.3): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.11.3 + dev: false + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1351,7 +1796,6 @@ packages: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1365,14 +1809,12 @@ packages: /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -1610,16 +2052,27 @@ packages: fsevents: 2.3.3 dev: true + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true /colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1641,6 +2094,11 @@ packages: engines: {node: '>=14'} dev: true + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true @@ -1802,6 +2260,10 @@ packages: semver: 7.5.4 dev: true + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + /entities@4.4.0: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} @@ -1978,6 +2440,11 @@ packages: '@esbuild/win32-x64': 0.20.0 dev: true + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: false + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2346,7 +2813,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -2362,6 +2828,11 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true @@ -2524,7 +2995,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /hast-util-from-parse5@8.0.1: resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} @@ -2616,6 +3086,11 @@ packages: space-separated-tokens: 2.0.2 dev: false + /hono@4.0.3: + resolution: {integrity: sha512-LLZFvqiksAO6Hf080XCUbsEjRxIrqEoiIN1HUzL0xtYP+BZUxGfeWg8yB5Dvp/sI7T0Lt4XdtGTGLwpIzAsl8w==} + engines: {node: '>=16.0.0'} + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -2660,6 +3135,15 @@ packages: resolve-from: 4.0.0 dev: true + /import-in-the-middle@1.7.1: + resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} + dependencies: + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + cjs-module-lexer: 1.2.3 + module-details-from-path: 1.0.3 + dev: false + /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2723,7 +3207,6 @@ packages: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 - dev: true /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -2737,6 +3220,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2921,14 +3409,21 @@ packages: p-locate: 5.0.0 dev: true + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false @@ -2944,7 +3439,6 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} @@ -3416,6 +3910,10 @@ packages: ufo: 1.3.2 dev: true + /module-details-from-path@1.0.3: + resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -3423,6 +3921,11 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + dev: true + /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3635,7 +4138,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -3666,13 +4168,12 @@ packages: pathe: 1.1.2 dev: true - /polka@1.0.0-next.22: - resolution: {integrity: sha512-a7tsZy5gFbJr0aUltZS97xCkbPglXuD67AMvTyZX7BTDBH384FWf0ZQF6rPvdutSxnO1vUlXM2zSLf5tCKk5RA==} - engines: {node: '>=8'} + /plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} dependencies: - '@polka/url': 1.0.0-next.21 - trouter: 3.2.0 - dev: false + queue-lit: 1.5.2 + dev: true /postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} @@ -3710,6 +4211,25 @@ packages: resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} dev: false + /protobufjs@7.2.6: + resolution: {integrity: sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.11.19 + long: 5.2.3 + dev: false + /pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} dev: true @@ -3719,6 +4239,11 @@ packages: engines: {node: '>=6'} dev: true + /queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -3743,11 +4268,6 @@ packages: functions-have-names: 1.2.3 dev: true - /regexparam@1.3.0: - resolution: {integrity: sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==} - engines: {node: '>=6'} - dev: false - /remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} dependencies: @@ -3790,6 +4310,22 @@ packages: unified: 11.0.4 dev: false + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /require-in-the-middle@7.2.0: + resolution: {integrity: sha512-3TLx5TGyAY6AOqLBoXmHkNql0HIf2RGbuMgCDT2WO/uGVAPJs6h7Kl+bN6TIZGd9bWhWPwnDnTHGtW8Iu77sdw==} + engines: {node: '>=8.6.0'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + module-details-from-path: 1.0.3 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3806,7 +4342,6 @@ packages: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -3895,7 +4430,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3909,6 +4443,10 @@ packages: engines: {node: '>=8'} dev: true + /shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + dev: false + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -3966,6 +4504,15 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} engines: {node: '>= 0.4'} @@ -4003,7 +4550,6 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -4042,7 +4588,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /templite@1.2.0: resolution: {integrity: sha512-O9BpPXF44a9Pg84Be6mjzlrqOtbP2I/B5PNLWu5hb1n9UQ1GTLsjdMg1z5ROCkF6NFXsO5LQfRXEpgTGrZ7Q0Q==} @@ -4113,12 +4658,17 @@ packages: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false - /trouter@3.2.0: - resolution: {integrity: sha512-rLLXbhTObLy2MBVjLC+jTnoIKw99n0GuJs9ov10J870vDw5qhTurPzsDrudNtBf5w/CZ9ctZy2p2IMmhGcel2w==} - engines: {node: '>=6'} + /tsc-alias@1.8.8: + resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} + hasBin: true dependencies: - regexparam: 1.3.0 - dev: false + chokidar: 3.5.3 + commander: 9.5.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + dev: true /tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -4226,7 +4776,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /unified@11.0.4: resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} @@ -4502,13 +5051,26 @@ packages: stackback: 0.0.2 dev: true + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml-eslint-parser@1.2.2: resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} @@ -4524,6 +5086,24 @@ packages: engines: {node: '>= 14'} dev: true + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: false + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/application/App.ts b/src/application/App.ts index eb5a983..cc65e21 100644 --- a/src/application/App.ts +++ b/src/application/App.ts @@ -1,5 +1,7 @@ +import { Server, createServer } from "http"; import { Bot, GrammyError, HttpError } from "grammy"; -import type { Polka } from "polka"; +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; import type { IDeploymentEvent, IIssueEvent, @@ -26,11 +28,12 @@ export type EventHandlerMapping = { export class App { private readonly _bot: Bot; private readonly _logger: ILogger; - private readonly _httpServer: Polka; + private readonly _hono: Hono; + private _httpServer: Server | undefined; private readonly _port: number; private readonly _routes: IRoute[]; - constructor(config: { port: number; bot: Bot; logger: ILogger; httpServer: Polka; routes: IRoute[] }) { + constructor(config: { port: number; bot: Bot; logger: ILogger; hono: Hono; routes: IRoute[] }) { if (!(config.bot instanceof Bot)) throw new Error("config.bot is not an instance of grammy bot."); if (config.logger === undefined || config.logger === null || typeof config.logger !== "object") { throw new Error("config.logger should be an instance implementing ILogger."); @@ -42,7 +45,7 @@ export class App { this._bot = config.bot; this._logger = config.logger; - this._httpServer = config.httpServer; + this._hono = config.hono; this._port = config.port; this._routes = config.routes; } @@ -88,57 +91,26 @@ export class App { }); } - private addJsonParser() { - this._httpServer.use(async (req, res, next) => { - try { - let body = ""; - - for await (const chunk of req) { - body += chunk; - } - - if (body === undefined || body.length === 0) { - res - .writeHead(422, { "Content-Type": "application/json" }) - .end(JSON.stringify({ msg: "Body shouldn't be empty" })); - return; - } - - switch (req.headers["content-type"]) { - case "application/x-www-form-urlencoded": { - const url = new URLSearchParams(body); - req.body = Object.fromEntries(url.entries()); - break; - } - case "application/json": - default: - req.body = JSON.parse(body); - } - next(); - } catch (error) { - res.writeHead(400, { "Content-Type": "application/json" }).end( - JSON.stringify({ - msg: "Invalid body content with the Content-Type header specification" - }) - ); - } - }); - } - public run() { this.addErrorHandling(); - this.addJsonParser(); this._bot.start({ onStart: (botInfo) => { this.registerServers(); this._logger.info(`@${botInfo.username} has been started.`); } }); + + this._httpServer = serve({ + fetch: this._hono.fetch, + port: this._port, + createServer: createServer + }, () => this._logger.info(`Server running on port ${this._port}`)) as Server; + this._httpServer.listen(this._port, () => this._logger.info(`Server running on port ${this._port}`)); } public stop() { this._bot.stop(); - this._httpServer.server.close(); + this._httpServer?.close(); } } \ No newline at end of file diff --git a/src/application/adapters/GithubAdapter.ts b/src/application/adapters/GithubAdapter.ts index 7e9c1d7..c78e398 100644 --- a/src/application/adapters/GithubAdapter.ts +++ b/src/application/adapters/GithubAdapter.ts @@ -1,4 +1,5 @@ import type { WebhookEvent } from "@octokit/webhooks-types"; +import { trace } from "@opentelemetry/api"; import type { Comment, CommentChanges, @@ -13,6 +14,8 @@ import type { WebhookEventName } from "~/application/webhook/types"; +const tracer = trace.getTracer("application.adaptersGithubAdapter"); + export class GithubAdapter { private readonly _repository?: Repository; private readonly _issue?: Issue; @@ -134,93 +137,97 @@ export class GithubAdapter { } get(eventName: WebhookEventName) { - // issue related events - if (eventName.startsWith("issue")) { - const payload = { - issue: this._issue, - repository: this._repository, - sender: this._sender - }; - - switch (eventName) { - case "issues.opened": - return payload as EventPayload["issues.opened"]; - case "issues.closed": - return payload as EventPayload["issues.closed"]; - case "issues.edited": - return payload as EventPayload["issues.edited"]; - case "issues.reopened": - return payload as EventPayload["issues.reopened"]; - case "issue_comment.created": - return { ...payload, comment: this._comment } as EventPayload["issue_comment.created"]; - case "issue_comment.edited": - return { ...payload, comment: this._comment, changes: this._changes } as EventPayload["issue_comment.edited"]; + return tracer.startActiveSpan("get", (span) => { + span.setAttribute("event_name", eventName); + + // issue related events + if (eventName.startsWith("issue")) { + const payload = { + issue: this._issue, + repository: this._repository, + sender: this._sender + }; + + switch (eventName) { + case "issues.opened": + return payload as EventPayload["issues.opened"]; + case "issues.closed": + return payload as EventPayload["issues.closed"]; + case "issues.edited": + return payload as EventPayload["issues.edited"]; + case "issues.reopened": + return payload as EventPayload["issues.reopened"]; + case "issue_comment.created": + return { ...payload, comment: this._comment } as EventPayload["issue_comment.created"]; + case "issue_comment.edited": + return { ...payload, comment: this._comment, changes: this._changes } as EventPayload["issue_comment.edited"]; + } } - } - - // pull request related events - if (eventName.startsWith("pull_request.")) { - const payload = { - pullRequest: this._pullRequest, - repository: this._repository, - review: this._review, - sender: this._sender - }; - switch (eventName) { - case "pull_request.opened": - return payload as EventPayload["pull_request.opened"]; - case "pull_request.closed": - return payload as EventPayload["pull_request.closed"]; - case "pull_request.edited": - return payload as EventPayload["pull_request.edited"]; + + // pull request related events + if (eventName.startsWith("pull_request.")) { + const payload = { + pullRequest: this._pullRequest, + repository: this._repository, + review: this._review, + sender: this._sender + }; + switch (eventName) { + case "pull_request.opened": + return payload as EventPayload["pull_request.opened"]; + case "pull_request.closed": + return payload as EventPayload["pull_request.closed"]; + case "pull_request.edited": + return payload as EventPayload["pull_request.edited"]; + } } - } - - // pr review related events - if (eventName.startsWith("pull_request_review")) { - const payload = { - pullRequest: this._pullRequest, - repository: this._repository, - review: this._review, - sender: this._sender - }; - switch (eventName) { - case "pull_request_review.edited": - return { ...payload, review: this._review } as EventPayload["pull_request_review.edited"]; - case "pull_request_review.submitted": - return { ...payload, sender: this._sender } as EventPayload["pull_request_review.submitted"]; - case "pull_request_review_comment.created": - return { - ...payload, - sender: this._sender, - comment: this._comment - } as EventPayload["pull_request_review_comment.created"]; + + // pr review related events + if (eventName.startsWith("pull_request_review")) { + const payload = { + pullRequest: this._pullRequest, + repository: this._repository, + review: this._review, + sender: this._sender + }; + switch (eventName) { + case "pull_request_review.edited": + return { ...payload, review: this._review } as EventPayload["pull_request_review.edited"]; + case "pull_request_review.submitted": + return { ...payload, sender: this._sender } as EventPayload["pull_request_review.submitted"]; + case "pull_request_review_comment.created": + return { + ...payload, + sender: this._sender, + comment: this._comment + } as EventPayload["pull_request_review_comment.created"]; + } } - } - - // deployment related events - if (eventName.startsWith("deployment")) { - switch (eventName) { - case "deployment_status": - return { - repository: this._repository, - deploymentStatus: this._deploymentStatus - } as EventPayload["deployment_status"]; + + // deployment related events + if (eventName.startsWith("deployment")) { + switch (eventName) { + case "deployment_status": + return { + repository: this._repository, + deploymentStatus: this._deploymentStatus + } as EventPayload["deployment_status"]; + } } - } - - // release related events - if (eventName.startsWith("release.")) { - switch (eventName) { - case "release.published": - return { - release: this._release, - repository: this._repository, - sender: this._sender - } as EventPayload["release.published"]; + + // release related events + if (eventName.startsWith("release.")) { + switch (eventName) { + case "release.published": + return { + release: this._release, + repository: this._repository, + sender: this._sender + } as EventPayload["release.published"]; + } } - } - - throw new Error(`Unhandle event. Event name: ${eventName}`); + + throw new Error(`Unhandle event. Event name: ${eventName}`); + }); } } \ No newline at end of file diff --git a/src/application/webhook/GithubWebhook.ts b/src/application/webhook/GithubWebhook.ts index 1acaf86..3f78916 100644 --- a/src/application/webhook/GithubWebhook.ts +++ b/src/application/webhook/GithubWebhook.ts @@ -1,10 +1,13 @@ import { createHmac, timingSafeEqual } from "node:crypto"; import type { WebhookEvent } from "@octokit/webhooks-types"; +import { trace } from "@opentelemetry/api"; import { GithubAdapter } from "../adapters/GithubAdapter"; import type { ILogger } from "../interfaces/ILogger"; import type { HandlerFunction, IWebhook, WebhookEventName } from "./types"; import { IGNORE_PRIVATE_REPOSITORY } from "~/env"; +const tracer = trace.getTracer("application.webhook.GithubWebhook"); + export class GithubWebhook implements IWebhook { public readonly secretToken: string; private readonly _handlers: Partial[]>> = {}; @@ -16,52 +19,62 @@ export class GithubWebhook implements IWebhook { this._logger = logger; } - public async verify(payload: string, signature: string) { - if (payload.length === 0 || signature.length === 0) throw Error("payload or signature wasn't provided."); - - const signatureBuffer = Buffer.from(signature); - const verificationBuffer = Buffer.from(await this.sign(payload)); - - if (signatureBuffer.length !== verificationBuffer.length) { - return false; - } - - return timingSafeEqual(signatureBuffer, verificationBuffer); + public verify(payload: string, signature: string) { + return tracer.startActiveSpan("verify", async () => { + if (payload.length === 0 || signature.length === 0) throw Error("payload or signature wasn't provided."); + + const signatureBuffer = Buffer.from(signature); + const verificationBuffer = Buffer.from(await this.sign(payload)); + + if (signatureBuffer.length !== verificationBuffer.length) { + return false; + } + + return timingSafeEqual(signatureBuffer, verificationBuffer); + }); } public sign(payload: string): Promise { - if (payload.length === 0) throw Error("payload wasn't provided."); - return Promise.resolve(`sha256=${createHmac("sha256", this.secretToken).update(payload).digest("hex")}`); + return tracer.startActiveSpan("sign", () => { + if (payload.length === 0) throw Error("payload wasn't provided."); + return Promise.resolve(`sha256=${createHmac("sha256", this.secretToken).update(payload).digest("hex")}`); + }); } public on(event: E, handler: HandlerFunction): void { - if (this._handlers[event] === undefined) { - this._handlers[event] = []; - } - this._handlers[event]!.push(handler as HandlerFunction); + return tracer.startActiveSpan("on", (span) => { + span.setAttribute("event", event); + if (this._handlers[event] === undefined) { + this._handlers[event] = []; + } + this._handlers[event]!.push(handler as HandlerFunction); + }); } - public async handle(eventName: WebhookEventName, payload: WebhookEvent, targetsId: bigint[]): Promise { - const handlers = this._handlers[eventName] as HandlerFunction[]; - - // no handler available - if (handlers === undefined || handlers.length === 0) { - this._logger.warn(`No handler available for ${eventName}`); - return; - } - - // Ignore event from private repository if IGNORE_PRIVATE_REPOSITORY is set to not empty - if ("repository" in payload && payload.repository !== undefined && payload.repository.private && IGNORE_PRIVATE_REPOSITORY) { - this._logger.info(`Ignored private repository for ${eventName}`); - return; - } - - const adapter = new GithubAdapter(payload); - - for await (const handler of handlers) { - handler({ type: eventName, payload: adapter.get(eventName), targetsId: targetsId }); - } - - this._logger.info(`Handled: ${eventName} to ${targetsId.join(" ")}`); + public handle(eventName: WebhookEventName, payload: WebhookEvent, targetsId: bigint[]): Promise { + return tracer.startActiveSpan("handle", async (span) => { + span.setAttribute("event_name", eventName); + const handlers = this._handlers[eventName] as HandlerFunction[]; + + // no handler available + if (handlers === undefined || handlers.length === 0) { + this._logger.warn(`No handler available for ${eventName}`); + return; + } + + // Ignore event from private repository if IGNORE_PRIVATE_REPOSITORY is set to not empty + if ("repository" in payload && payload.repository !== undefined && payload.repository.private && IGNORE_PRIVATE_REPOSITORY) { + this._logger.info(`Ignored private repository for ${eventName}`); + return; + } + + const adapter = new GithubAdapter(payload); + + for await (const handler of handlers) { + handler({ type: eventName, payload: adapter.get(eventName), targetsId: targetsId }); + } + + this._logger.info(`Handled: ${eventName} to ${targetsId.join(" ")}`); + }); } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ab8c4fb..63fb5fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ import { readFile } from "fs/promises"; import path from "path"; import { Bot } from "grammy"; import { parse as parseGura } from "gura"; -import polka from "polka"; +import { Hono } from "hono"; +import { otelTracer } from "./utils/honoOtelTracer"; import { appConfigSchema } from "~/schema"; import { BOT_TOKEN, GITHUB_WEBHOOK_SECRET, PORT, HOME_GROUP } from "~/env"; import { InMemoryGroupMapping } from "~/infrastructure/InMemoryGroupMapping"; @@ -53,26 +54,27 @@ const eventHandlers: EventHandlerMapping = { }; // webhook server and handlers -const polkaInstance = polka(); -polkaInstance.get("/", (_, res) => void res.writeHead(200).end("OK")); -const githubRoute = new GithubRoute(polkaInstance, { +const serverInstance = new Hono(); +serverInstance.get("/", (c) => c.text("OK")); +serverInstance.use("*", otelTracer("gitgram")); +const githubRoute = new GithubRoute(serverInstance, { path: "/github", webhook: new GithubWebhook(GITHUB_WEBHOOK_SECRET, logger), handlers: eventHandlers, groupMapping: groupMapping }); -// main bot app instance +// main bot app instances const app = new App({ - httpServer: polkaInstance, + hono: serverInstance, port: parseInt(PORT), bot, logger, routes: [githubRoute] }); -app.run(); - // Enable graceful shutdown process.once("SIGINT", () => app.stop()); -process.once("SIGTERM", () => app.stop()); \ No newline at end of file +process.once("SIGTERM", () => app.stop()); + +app.run(); \ No newline at end of file diff --git a/src/presentation/TelegramPresenter.ts b/src/presentation/TelegramPresenter.ts index 2079a31..02d8aaa 100644 --- a/src/presentation/TelegramPresenter.ts +++ b/src/presentation/TelegramPresenter.ts @@ -1,3 +1,4 @@ +import * as Sentry from "@sentry/node"; import type { Bot } from "grammy"; import { bufferTime, distinctUntilKeyChanged, groupBy, mergeMap, Subject } from "rxjs"; import type { IPresenter, MessageData } from "~/application/interfaces/IPresenter"; @@ -57,6 +58,7 @@ export class TelegramPresenter implements IPresenter { } }); } catch (err: unknown) { + Sentry.captureException(err); if (err instanceof Error) { this._logger.error(err.message); } diff --git a/src/presentation/event-handlers/Deployment.ts b/src/presentation/event-handlers/Deployment.ts index b62eaa4..58d692d 100644 --- a/src/presentation/event-handlers/Deployment.ts +++ b/src/presentation/event-handlers/Deployment.ts @@ -1,9 +1,12 @@ +import { trace } from "@opentelemetry/api"; import { z } from "zod"; import type { IDeploymentEvent } from "~/application/interfaces/events/IDeploymentEvent"; import type { IPresenter } from "~/application/interfaces/IPresenter"; import type { HandlerFunction } from "~/application/webhook/types"; import { interpolate } from "~/utils/interpolate"; +const tracer = trace.getTracer("presentation.event-handlers.Deployment"); + export const deploymentTemplateSchema = z.object({ status: z.object({ base: z.string(), @@ -19,23 +22,25 @@ export class DeploymentEventHandler implements IDeploymentEvent { status(): HandlerFunction<"deployment_status"> { return (event) => { - const description = event.payload.deploymentStatus.description; - const response = interpolate( - this._templates.status.statuses[event.payload.deploymentStatus.state.toLowerCase()] + - this._templates.status.base, - { - repoName: event.payload.repository.fullName, - environment: event.payload.deploymentStatus.environment, - description: - (description.length > 100 ? description.slice(0, 100) : description) || "No description provided", - targetUrl: event.payload.deploymentStatus?.targetUrl ?? "No target URL provided" - } - ); - - this._hub.send({ - event: "deployment_status", - targetsId: event.targetsId, - payload: response + return tracer.startActiveSpan("status", () => { + const description = event.payload.deploymentStatus.description; + const response = interpolate( + this._templates.status.statuses[event.payload.deploymentStatus.state.toLowerCase()] + + this._templates.status.base, + { + repoName: event.payload.repository.fullName, + environment: event.payload.deploymentStatus.environment, + description: + (description.length > 100 ? description.slice(0, 100) : description) || "No description provided", + targetUrl: event.payload.deploymentStatus?.targetUrl ?? "No target URL provided" + } + ); + + this._hub.send({ + event: "deployment_status", + targetsId: event.targetsId, + payload: response + }); }); }; } diff --git a/src/presentation/event-handlers/Discussion.ts b/src/presentation/event-handlers/Discussion.ts index 4ae997c..35d0869 100644 --- a/src/presentation/event-handlers/Discussion.ts +++ b/src/presentation/event-handlers/Discussion.ts @@ -1,9 +1,12 @@ +import { trace } from "@opentelemetry/api"; import { z } from "zod"; import type { IPresenter } from "~/application/interfaces/IPresenter"; import type { IDiscussionEvent } from "~/application/interfaces/events/IDiscussionEvent"; import type { HandlerFunction } from "~/application/webhook/types"; import { interpolate } from "~/utils/interpolate"; +const tracer = trace.getTracer("presentation.event-handlers.Discussion"); + export const discussionTemplateSchema = z.object({ created: z.string().trim(), closed: z.string().trim(), @@ -24,120 +27,132 @@ export class DiscussionEventHandler implements IDiscussionEvent { created(): HandlerFunction<"discussion.created"> { return (event) => { - const message = this._templates.created; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - author: event.payload.discussion.user.name - }); - this._hub.send({ - payload: payload, - event: "discussion.created", - targetsId: event.targetsId + return tracer.startActiveSpan("created", () => { + const message = this._templates.created; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + author: event.payload.discussion.user.name + }); + this._hub.send({ + payload: payload, + event: "discussion.created", + targetsId: event.targetsId + }); }); }; } closed(): HandlerFunction<"discussion.closed"> { return (event) => { - const message = this._templates.closed; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - actor: event.payload.sender.name - }); - this._hub.send({ - payload: payload, - event: "discussion.closed", - targetsId: event.targetsId + return tracer.startActiveSpan("closed", () => { + const message = this._templates.closed; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + actor: event.payload.sender.name + }); + this._hub.send({ + payload: payload, + event: "discussion.closed", + targetsId: event.targetsId + }); }); }; } reopened(): HandlerFunction<"discussion.reopened"> { return (event) => { - const message = this._templates.reopened; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - actor: event.payload.sender.name - }); - this._hub.send({ - payload: payload, - event: "discussion.reopened", - targetsId: event.targetsId + return tracer.startActiveSpan("reopened", () => { + const message = this._templates.reopened; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + actor: event.payload.sender.name + }); + this._hub.send({ + payload: payload, + event: "discussion.reopened", + targetsId: event.targetsId + }); }); }; } edited(): HandlerFunction<"discussion.edited"> { return (event) => { - const message = this._templates.edited; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - actor: event.payload.sender.name - }); - this._hub.send({ - payload: payload, - event: "discussion.edited", - targetsId: event.targetsId + return tracer.startActiveSpan("edited", () => { + const message = this._templates.edited; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + actor: event.payload.sender.name + }); + this._hub.send({ + payload: payload, + event: "discussion.edited", + targetsId: event.targetsId + }); }); }; } deleted(): HandlerFunction<"discussion.deleted"> { return (event) => { - const message = this._templates.deleted; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - actor: event.payload.sender.name - }); - this._hub.send({ - payload: payload, - event: "discussion.deleted", - targetsId: event.targetsId + return tracer.startActiveSpan("deleted", () => { + const message = this._templates.deleted; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + actor: event.payload.sender.name + }); + this._hub.send({ + payload: payload, + event: "discussion.deleted", + targetsId: event.targetsId + }); }); }; } pinned(): HandlerFunction<"discussion.pinned"> { return (event) => { - const message = this._templates.pinned; - const payload = interpolate(message, { - url: event.payload.discussion.url, - no: event.payload.discussion.number, - title: event.payload.discussion.title, - body: event.payload.discussion.body, - repoName: event.payload.repository.fullName, - category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, - actor: event.payload.sender.name - }); - this._hub.send({ - payload: payload, - event: "discussion.pinned", - targetsId: event.targetsId + return tracer.startActiveSpan("pinned", () => { + const message = this._templates.pinned; + const payload = interpolate(message, { + url: event.payload.discussion.url, + no: event.payload.discussion.number, + title: event.payload.discussion.title, + body: event.payload.discussion.body, + repoName: event.payload.repository.fullName, + category: `${event.payload.discussion.category.emoji} ${event.payload.discussion.category.name}`, + actor: event.payload.sender.name + }); + this._hub.send({ + payload: payload, + event: "discussion.pinned", + targetsId: event.targetsId + }); }); }; } diff --git a/src/presentation/routes/GithubRoute.ts b/src/presentation/routes/GithubRoute.ts index 5360f03..0d5105d 100644 --- a/src/presentation/routes/GithubRoute.ts +++ b/src/presentation/routes/GithubRoute.ts @@ -1,12 +1,12 @@ import type { WebhookEvent } from "@octokit/webhooks-types"; -import type { NextHandler, Polka, Request, Response } from "polka"; +import { Hono, type Context, type Next } from "hono"; import type { ServerConfig } from "./types"; import type { IRoute } from "~/application/interfaces/IRoute"; import type { WebhookEventName } from "~/application/webhook/types"; export class GithubRoute implements IRoute { // eslint-disable-next-line no-useless-constructor - constructor(private readonly _polka: Polka, private readonly _config: ServerConfig) { } + constructor(private readonly _server: Hono, private readonly _config: ServerConfig) { } public register() { // handle issues events @@ -41,34 +41,34 @@ export class GithubRoute implements IRoute { this._config.webhook.on("discussion_comment.created", this._config.handlers.discussion.commentCreated()); // attach routes - this._polka.post(this._config.path, this.verifySignature.bind(this), this.handleWebhook.bind(this)); + this._server.use(this._config.path, this.verifySignature.bind(this)); + this._server.post(this._config.path, this.handleWebhook.bind(this)); } - private handleWebhook(req: Request, res: Response) { - const event = req.headers["x-github-event"]; - const eventType = req.body.action !== undefined ? `.${req.body.action}` : ""; + private async handleWebhook(c: Context) { + const requestBody = await c.req.json(); + const event = c.req.header("x-github-event"); + const eventType = requestBody.action !== undefined ? `.${requestBody.action}` : ""; const eventName = `${event}${eventType}` as WebhookEventName; - const targetIds = this._config.groupMapping.findGroupsIn(req.body.repository.html_url); + const targetIds = this._config.groupMapping.findGroupsIn(requestBody.repository.html_url); // reply back first so github knows we've received it before waiting us handling the response - res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ msg: "OK" })); - this._config.webhook.handle(eventName, req.body, targetIds); + this._config.webhook.handle(eventName, requestBody, targetIds); + return c.json({ msg: "OK" }, 200); } - private async verifySignature(req: Request, res: Response, next: NextHandler) { - const hubSignature = req.headers["x-hub-signature-256"] as string | undefined; + private async verifySignature(c: Context, next: Next) { + const hubSignature = c.req.header("x-hub-signature-256"); if (hubSignature === undefined || hubSignature.length === 0) { - res - .writeHead(401, { "Content-Type": "application/json" }) - .end(JSON.stringify({ msg: "Signature length is not the same." })); - return; + return c.json({ msg: "Signature length is not the same." }, 401); } - const isEqual = await this._config.webhook.verify(JSON.stringify(req.body), hubSignature); + const requestBody = await c.req.json(); + + const isEqual = await this._config.webhook.verify(JSON.stringify(requestBody), hubSignature); if (!isEqual) { - res.writeHead(401, { "Content-Type": "application/json" }).end(JSON.stringify({ msg: "Invalid signature" })); - return; + return c.json({ msg: "Invalid signature" }, 401); } - next(); + return next(); } } \ No newline at end of file diff --git a/src/presentation/routes/GitlabRoute.ts b/src/presentation/routes/GitlabRoute.ts index 5bb7768..cac9f4f 100644 --- a/src/presentation/routes/GitlabRoute.ts +++ b/src/presentation/routes/GitlabRoute.ts @@ -1,38 +1,35 @@ -import type { Polka, Request, Response } from "polka"; +import { Hono, type Context, type Next } from "hono"; import type { ServerConfig } from "./types"; import type { IRoute } from "~/application/interfaces/IRoute"; import type { WebhookEventName } from "~/application/webhook/types"; export class GitlabRoute implements IRoute { // eslint-disable-next-line no-useless-constructor - constructor(private readonly _polka: Polka, private readonly _config: ServerConfig) {} + constructor(private readonly _server: Hono, private readonly _config: ServerConfig) {} public register() { // TODO: register supported events here // attach routes - this._polka.post(this._config.path, this.verifySignature.bind(this), this.handleWebhook.bind(this)); + this._server.use(this._config.path, this.verifySignature.bind(this)); + this._server.post(this._config.path, this.handleWebhook.bind(this)); } - private handleWebhook(req: Request, res: Response) { - const eventName = `${req.body.event}.${req.body.payload.action}` as WebhookEventName; - const targetId = this._config.groupMapping.findGroupsIn(req.body.payload.repository.html_url); - // reply back first so github knows we've received it before waiting us handling the response - res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ msg: "OK" })); - this._config.webhook.handle(eventName, req.body.payload, targetId); + private async handleWebhook(c: Context) { + const requestBody = await c.req.json(); + const eventName = `${requestBody.event}.${requestBody.payload.action}` as WebhookEventName; + const targetId = this._config.groupMapping.findGroupsIn(requestBody.payload.repository.html_url); + this._config.webhook.handle(eventName, requestBody.payload, targetId); + return c.json({ msg: "OK" }, 200); } - private verifySignature(req: Request, res: Response) { - const hubSignature = req.headers["x-gitlab-token"] as string | undefined; + private async verifySignature(c: Context, next: Next) { + const hubSignature = c.req.header("x-gitlab-token"); if (hubSignature === undefined || hubSignature.length === 0) { - res - .writeHead(401, { "Content-Type": "application/json" }) - .end(JSON.stringify({ msg: "Signature length is not the same." })); - return; + return c.json({ msg: "Signature length is not the same." }, 401); } - // TODO: implementation - throw new Error("Not implemented"); + return next(); } } \ No newline at end of file diff --git a/src/tracing.ts b/src/tracing.ts new file mode 100644 index 0000000..fbb7a7e --- /dev/null +++ b/src/tracing.ts @@ -0,0 +1,49 @@ +// Sentry dependencies +import Sentry from "@sentry/node"; +import { getClient, setupGlobalHub, SentryPropagator, SentrySampler, SentrySpanProcessor, setupEventContextTrace, wrapContextManagerClass, setOpenTelemetryContextAsyncContextStrategy } from "@sentry/opentelemetry"; + +// OpenTelemetry dependencies +import opentelemetry from "@opentelemetry/sdk-node"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc"; +import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks"; +import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; + +function setupSentry() { + setupGlobalHub(); + + // Make sure to call `Sentry.init` BEFORE initializing the OpenTelemetry SDK + Sentry.init({ + dsn: "", + sampleRate: 1.0, + tracesSampleRate: 0.3, + // set the instrumenter to use OpenTelemetry instead of Sentry + instrumenter: "otel" + }); + + const client = getClient()!; + setupEventContextTrace(client); + + // You can wrap whatever local storage context manager you want to use + const SentryContextManager = wrapContextManagerClass( + AsyncLocalStorageContextManager + ); + + const sdk = new opentelemetry.NodeSDK({ + // Existing config + traceExporter: new OTLPTraceExporter(), + instrumentations: [new HttpInstrumentation()], + + // Sentry config + spanProcessor: new SentrySpanProcessor(), + textMapPropagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + sampler: new SentrySampler(client) + }); + + // Ensure OpenTelemetry Context & Sentry Hub/Scope is synced + setOpenTelemetryContextAsyncContextStrategy(); + + sdk.start(); +} + +setupSentry(); \ No newline at end of file diff --git a/src/utils/honoOtelTracer.ts b/src/utils/honoOtelTracer.ts new file mode 100644 index 0000000..84792f7 --- /dev/null +++ b/src/utils/honoOtelTracer.ts @@ -0,0 +1,53 @@ +import { trace } from "@opentelemetry/api"; +import type { Span } from "@opentelemetry/api"; +import type { Context, Next } from "hono"; +import { createMiddleware } from "hono/factory"; +import * as Sentry from "@sentry/node"; + +const recordError = (span: Span, error: unknown) => { + if (error instanceof Error) { + span.recordException({ + name: error.name, + message: error.message + }); + span.setStatus({ code: 2, message: error.message }); + } else { + const errorMessage = String(error); + span.recordException({ message: errorMessage }); + span.setStatus({ code: 2, message: errorMessage }); + } +}; + +export const otelTracer = ( + tracerName: string, + customAttributes?: (context: Context) => Record +) => { + const tracer = trace.getTracer(tracerName); + return createMiddleware(async (c: Context, next: Next) => { + const span = tracer.startSpan("http.server", { + attributes: { + "http.request.method": c.req.method, + "http.request.url": c.req.url, + ...customAttributes + } + }); + const startTime = Date.now(); + + try { + await next(); + span.setAttribute("http.response.status_code", c.res.status); + } catch (error) { + recordError(span, error); + if (c.req.header("User-Agent")) { + const userAgent = c.req.header("User-Agent") || "Unknown"; + span.setAttribute("http.user_agent", userAgent); + } + Sentry.captureException(error); + throw error; + } finally { + const duration = Date.now() - startTime; + span.setAttribute("http.request_duration", duration); + span.end(); + } + }); +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e82516a..a60be3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "sourceMap": true, "outDir": "./dist", "removeComments": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "newLine": "lf", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, @@ -47,5 +47,9 @@ /* Completeness */ "skipDefaultLibCheck": false, "skipLibCheck": true + }, + "tsc-alias": { + "resolveFullPaths": true, + "verbose": true } }