diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 3f9841d..49110de 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -69,10 +69,41 @@ jobs: -H "Accept: application/json" \ -H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" + sentry-release: + runs-on: ubuntu-latest + needs: [deploy-staging, deploy-prod] + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Create and Finalize Sentry Release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + with: + version: ${{ github.sha }} + environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + finalize: true + set_commits: auto + + - name: Mark Release as Deployed + run: | + ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + VERSION=${{ github.sha }} + curl https://sentry.io/api/0/organizations/${{ secrets.SENTRY_ORG }}/releases/$VERSION/deploys/ \ + -X POST \ + -H "Authorization: Bearer ${{ secrets.SENTRY_AUTH_TOKEN }}" \ + -H 'Content-Type: application/json' \ + -d "{\"environment\":\"$ENVIRONMENT\"}" + + notify: runs-on: ubuntu-latest - needs: [build-test-scan, deploy-staging, deploy-prod] - if: always() + needs: [build-test-scan, deploy-staging, deploy-prod, sentry-release] + if: always() steps: - name: Slack Notification for Staging if: github.ref == 'refs/heads/develop' @@ -88,6 +119,7 @@ jobs: Commit: ${{ github.sha }} Status: ${{ job.status }} Environment: Staging + Release: ${{ github.sha }} - name: Slack Notification for Production if: github.ref == 'refs/heads/main' @@ -103,3 +135,4 @@ jobs: Commit: ${{ github.sha }} Status: ${{ job.status }} Environment: Production + Release: ${{ github.sha }} diff --git a/package-lock.json b/package-lock.json index a41f6cf..7464de7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@sentry/node": "^7.120.4", + "@sentry/tracing": "^7.120.4", "express": "^4.21.2" }, "devDependencies": { @@ -1152,6 +1154,97 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz", + "integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz", + "integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz", + "integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.4.tgz", + "integrity": "sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.4", + "@sentry/core": "7.120.4", + "@sentry/integrations": "7.120.4", + "@sentry/types": "7.120.4", + "@sentry/utils": "7.120.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/tracing": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.120.4.tgz", + "integrity": "sha512-cAtpLh23qW3hoqZJ6c36EvFki5NhFWUSK71ALHefqDXEocMlfDc9I+IGn3B/ola2D2TDEDamCy3x32vctKqOag==", + "license": "MIT", + "dependencies": { + "@sentry-internal/tracing": "7.120.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz", + "integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.4", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz", + "integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==", + "license": "MIT", + "dependencies": { + "@sentry/types": "7.120.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3232,6 +3325,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4210,6 +4309,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4374,6 +4482,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index fd58148..627923a 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "author": "Your Name", "license": "MIT", "dependencies": { + "@sentry/node": "^7.120.4", + "@sentry/tracing": "^7.120.4", "express": "^4.21.2" }, "devDependencies": { @@ -29,5 +31,15 @@ }, "directories": { "test": "tests" + }, + "jest": { + "testEnvironment": "node", + "setupFiles": [ + "/tests/jest.setup.js" + ], + "moduleNameMapper": { + "^@sentry/node$": "/tests/__mocks__/@sentry/node.js", + "^@sentry/tracing$": "/tests/__mocks__/@sentry/tracing.js" + } } } diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..7db226d --- /dev/null +++ b/render.yaml @@ -0,0 +1,32 @@ +services: + - type: web + name: mydev-staging + env: docker + plan: free + branch: develop + rootDir: . + dockerfilePath: ./Dockerfile + autoDeploy: true + envVars: + - key: NODE_ENV + value: staging + - key: PORT + value: 10000 + - key: SENTRY_DSN + sync: false + + - type: web + name: mydev-prod + env: docker + plan: free + branch: main + rootDir: . + dockerfilePath: ./Dockerfile + autoDeploy: true + envVars: + - key: NODE_ENV + value: production + - key: PORT + value: 10000 + - key: SENTRY_DSN + sync: false diff --git a/src/app.js b/src/app.js index 9e9f6f6..11f2d61 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,34 @@ const express = require("express"); +const Sentry = require("@sentry/node"); +const Tracing = require("@sentry/tracing"); + const app = express(); +// Initialize Sentry (v7 API) +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + integrations: [ + new Tracing.Integrations.Express({ app }), // works fine with v7 + ], + tracesSampleRate: 1.0, // lower this in prod (e.g. 0.1) +}); + +// Request handler (before routes) +app.use(Sentry.Handlers.requestHandler()); +app.use(Sentry.Handlers.tracingHandler()); + +// Example route app.get("/", (req, res) => { - res.send("You are safe in Wizfi's pipeline"); + res.send("You are safe in Wizfi's Pipeline!"); }); +// Debug route to test Sentry (ESLint safe) +app.get("/debug-sentry", (req, res) => { + res.status(500).send("Triggering Sentry debug error..."); + throw new Error("Debug Sentry error!"); +}); + +// Error handler (after routes) +app.use(Sentry.Handlers.errorHandler()); + module.exports = app; diff --git a/tests/__mocks__/@sentry/node.js b/tests/__mocks__/@sentry/node.js new file mode 100644 index 0000000..c40070e --- /dev/null +++ b/tests/__mocks__/@sentry/node.js @@ -0,0 +1,9 @@ +module.exports = { + init: jest.fn(), + Handlers: { + requestHandler: () => (req, res, next) => next(), + tracingHandler: () => (req, res, next) => next(), + errorHandler: () => (err, req, res, next) => next(err), + }, + Integrations: {}, +}; diff --git a/tests/__mocks__/@sentry/tracing.js b/tests/__mocks__/@sentry/tracing.js new file mode 100644 index 0000000..ba10fea --- /dev/null +++ b/tests/__mocks__/@sentry/tracing.js @@ -0,0 +1,7 @@ +module.exports = { + Integrations: { + Express: function () { + return {}; + }, + }, +}; diff --git a/tests/index.test.js b/tests/index.test.js index ab5f12f..2ee02c9 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,9 +1,15 @@ const request = require("supertest"); const app = require("../src/app"); -describe("GET /", () => { - it("should return Hello message", async () => { +describe("App Routes", () => { + it("should return Hello from Wizfi pipeline!", async () => { const res = await request(app).get("/"); - expect(res.text).toBe("You are safe in Wizfi's pipeline"); + expect(res.statusCode).toBe(200); + expect(res.text).toBe("You are safe in Wizfi's Pipeline!"); + }); + + it("should handle a 404 route", async () => { + const res = await request(app).get("/not-found"); + expect(res.statusCode).toBe(404); }); }); diff --git a/tests/jest.setup.js b/tests/jest.setup.js new file mode 100644 index 0000000..88f60d8 --- /dev/null +++ b/tests/jest.setup.js @@ -0,0 +1,2 @@ +// Fake DSN so Sentry.init() doesn't crash in test runs +process.env.SENTRY_DSN = "http://fake-dsn.local";