From b2adbb715e4556363994a8db31e646ab0cddb29b Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Thu, 11 Sep 2025 17:37:32 -0400 Subject: [PATCH] feat: crash details by group --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 +- package.json | 9 +- .../crash-api-client/crash-api-client.e2e.ts | 14 +++ .../crash-api-client/crash-api-client.spec.ts | 86 +++++++++++++++++++ .../crash-api-client/crash-api-client.ts | 34 ++++++++ 6 files changed, 140 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55d785f..026bf3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: "20.x" + node-version: "22.x" - name: Install run: npm install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2475d5b..19baaeb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,10 +14,10 @@ jobs: with: persist-credentials: false - # Pin to Node.js 22.12 as 22.18+ breaks tsconfig-paths/register functionality + # Use latest Node.js 22 LTS - uses: actions/setup-node@v4 with: - node-version: "22.12" + node-version: "22.x" - name: Install run: npm install diff --git a/package.json b/package.json index 846460d..aeb70ec 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,14 @@ "files": [ "dist" ], - "engines": { - "node": ">=18.0.0 <22.13.0" - }, "scripts": { "prepare": "husky install", "pretest": "npm run lint:fix", - "test": "ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.spec.json", + "test": "NODE_OPTIONS='--no-experimental-strip-types' ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.spec.json", "lint": "eslint .", "lint:fix": "npm run lint -- --fix", - "e2e": "ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.e2e.json", - "e2e:teamcity": "ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.teamcity.e2e.json", + "e2e": "NODE_OPTIONS='--no-experimental-strip-types' ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.e2e.json", + "e2e:teamcity": "NODE_OPTIONS='--no-experimental-strip-types' ts-node -r tsconfig-paths/register node_modules/jasmine/bin/jasmine --config=spec/support/jasmine.teamcity.e2e.json", "release": "npm run build && npm publish --access public", "build": "npm run build:cjs && npm run build:esm", "build:cjs": "tsc -p tsconfig.cjs.json && tsconfig-replace-paths -p tsconfig.json -o ./dist/cjs -s ./src", diff --git a/src/crash/crash-api-client/crash-api-client.e2e.ts b/src/crash/crash-api-client/crash-api-client.e2e.ts index ded9bb4..d59c154 100644 --- a/src/crash/crash-api-client/crash-api-client.e2e.ts +++ b/src/crash/crash-api-client/crash-api-client.e2e.ts @@ -41,6 +41,20 @@ describe('CrashApiClient', () => { }); }); + describe('getCrashByGroupId', () => { + it('should return crash details for the given stack key group', async () => { + const response = await crashClient.getCrashByGroupId(config.database, stackKeyId); + + expect(response).toBeDefined(); + expect(typeof response.stackKeyId).toBe('number'); + expect(response.stackKeyId).toBeGreaterThan(0); + expect(response.appName).toEqual(application); + expect(response.appVersion).toBeDefined(); + expect(typeof response.appVersion).toBe('string'); + expect(response.appVersion.length).toBeGreaterThan(0); + }); + }); + describe('reprocessCrash', () => { it('should return 200 for a recent crash that has symbols', async () => { const response = await crashClient.reprocessCrash(config.database, id); diff --git a/src/crash/crash-api-client/crash-api-client.spec.ts b/src/crash/crash-api-client/crash-api-client.spec.ts index 8e3f02e..dba84d1 100644 --- a/src/crash/crash-api-client/crash-api-client.spec.ts +++ b/src/crash/crash-api-client/crash-api-client.spec.ts @@ -91,6 +91,92 @@ describe('CrashApiClient', () => { }); }); + describe('getCrashByGroupId', () => { + let client: CrashApiClient; + let fakeBugSplatApiClient; + let fakeCrashApiResponse; + let result; + const groupId = 12345; + + beforeEach(async () => { + fakeCrashApiResponse = createFakeCrashApiResponse(); + const fakeResponse = createFakeResponseBody(200, fakeCrashApiResponse); + fakeBugSplatApiClient = createFakeBugSplatApiClient(fakeFormData, fakeResponse); + client = new CrashApiClient(fakeBugSplatApiClient); + + result = await client.getCrashByGroupId(database, groupId); + }); + + it('should call fetch with correct route', () => { + expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( + '/api/crash/details', + jasmine.anything() + ); + }); + + it('should call fetch with formData containing database and stackKeyId', () => { + expect(fakeFormData.append).toHaveBeenCalledWith('database', database); + expect(fakeFormData.append).toHaveBeenCalledWith('stackKeyId', groupId.toString()); + expect(fakeBugSplatApiClient.fetch).toHaveBeenCalledWith( + jasmine.any(String), + jasmine.objectContaining({ + method: 'POST', + body: fakeFormData, + cache: 'no-cache', + credentials: 'include', + redirect: 'follow', + }) + ); + }); + + it('should return response json', () => { + expect(result).toEqual(jasmine.objectContaining(createCrashDetails(fakeCrashApiResponse))); + }); + + it('should throw if status is not 200', async () => { + const message = 'Bad request'; + + try { + const fakeErrorBody = { message }; + const fakeResponse = createFakeResponseBody(400, fakeErrorBody, false); + const fakeBugSplatApiClient = createFakeBugSplatApiClient(fakeFormData, fakeResponse); + const client = new CrashApiClient(fakeBugSplatApiClient); + + await client.getCrashByGroupId(database, groupId); + fail('getCrashByGroupId was supposed to throw!'); + } catch (error: any) { + expect(error.message).toEqual(message); + } + }); + + it('should throw if database is falsy', async () => { + try { + await client.getCrashByGroupId('', groupId); + fail('getCrashByGroupId was supposed to throw!'); + } catch (error: any) { + expect(error.message).toMatch(/to be a non white space string/); + } + }); + + it('should throw if groupId is less than or equal to 0', async () => { + try { + await client.getCrashByGroupId(database, 0); + fail('getCrashByGroupId was supposed to throw!'); + } catch (error: any) { + expect(error.message).toMatch(/to be a positive non-zero number/); + } + }); + + it('should throw if groupId is negative', async () => { + try { + await client.getCrashByGroupId(database, -1); + fail('getCrashByGroupId was supposed to throw!'); + } catch (error: any) { + expect(error.message).toMatch(/to be a positive non-zero number/); + } + }); + }); + describe('reprocessCrash', () => { let client: CrashApiClient; let fakeReprocessApiResponse; diff --git a/src/crash/crash-api-client/crash-api-client.ts b/src/crash/crash-api-client/crash-api-client.ts index d5539d8..20a22d3 100644 --- a/src/crash/crash-api-client/crash-api-client.ts +++ b/src/crash/crash-api-client/crash-api-client.ts @@ -10,6 +10,40 @@ import { export class CrashApiClient { constructor(private _client: ApiClient) {} + async getCrashByGroupId(database: string, groupId: number): Promise { + ac.assertNonWhiteSpaceString(database, 'database'); + if (groupId <= 0) { + throw new Error( + `Expected groupId to be a positive non-zero number. Value received: "${groupId}"` + ); + } + + const formData = this._client.createFormData(); + formData.append('database', database); + formData.append('stackKeyId', groupId.toString()); + + const init = { + method: 'POST', + body: formData, + cache: 'no-cache', + credentials: 'include', + redirect: 'follow', + duplex: 'half', + } as RequestInit; + + const response = await this._client.fetch( + '/api/crash/details', + init + ); + const json = await response.json(); + + if (response.status !== 200) { + throw new Error((json as Error).message); + } + + return createCrashDetails(json as CrashDetailsRawResponse); + } + async getCrashById(database: string, crashId: number): Promise { ac.assertNonWhiteSpaceString(database, 'database'); if (crashId <= 0) {