Skip to content

Commit 35aa34a

Browse files
bobbyg603claude
andauthored
feat: add crash count fields to versions API client (#160)
* feat: add DashboardApiClient for BFF endpoint Add a new DashboardApiClient that calls /api/dashboard.php to fetch all dashboard data in a single request. Includes request/response types and unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make startDate/endDate optional in DashboardApiRequest Support "All Time" queries where no date range is specified. Only includes date params in the URL when provided. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add totalThrottled fields to DashboardApiResponse Add totalThrottled and totalThrottledPrevious to the DashboardApiResponse interface to support rejected crashes on the dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add lastCrashTime to DashboardApiResponse Add `lastCrashTime: string | null` field to the response type to support the new unfiltered last crash time from the dashboard BFF. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add crash count fields to versions API client Add totalCrashCount and periodCrashCount to VersionsApiRow. Extend getVersions() to accept optional crashCountStartDate/crashCountEndDate params, and add extraParams support to TableDataClient.getData(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add tests for crash count fields and extraParams Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a1cfbde commit 35aa34a

File tree

6 files changed

+162
-3
lines changed

6 files changed

+162
-3
lines changed

src/common/data/table-data/table-data-client/table-data-client.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('TableDataClient', () => {
2424
'withSortColumn',
2525
'withSortOrder',
2626
'build',
27+
'entries',
2728
]);
2829
dataTableFormDataBuilder.withDatabase.and.returnValue(dataTableFormDataBuilder);
2930
dataTableFormDataBuilder.withColumnGroups.and.returnValue(dataTableFormDataBuilder);
@@ -33,6 +34,7 @@ describe('TableDataClient', () => {
3334
dataTableFormDataBuilder.withSortColumn.and.returnValue(dataTableFormDataBuilder);
3435
dataTableFormDataBuilder.withSortOrder.and.returnValue(dataTableFormDataBuilder);
3536
dataTableFormDataBuilder.build.and.returnValue(formData);
37+
dataTableFormDataBuilder.entries.and.returnValue(formData);
3638
spyOn(TableDataFormDataBuilder, 'TableDataFormDataBuilder').and.returnValue(
3739
dataTableFormDataBuilder
3840
);
@@ -155,4 +157,75 @@ describe('TableDataClient', () => {
155157
}
156158
});
157159
});
160+
161+
describe('getData (GET)', () => {
162+
let database;
163+
let result;
164+
165+
beforeEach(async () => {
166+
database = 'TestDatabase';
167+
const resp = await service.getData({ database });
168+
if (isErrorResponse(resp)) {
169+
throw new Error((await resp.json()).message);
170+
}
171+
result = await resp.json();
172+
});
173+
174+
it('should call fetch with a GET request using query params', () => {
175+
const expectedQueryString = new URLSearchParams(formData).toString();
176+
expect(bugSplatApiClient.fetch).toHaveBeenCalledWith(
177+
`${url}?${expectedQueryString}`,
178+
jasmine.objectContaining({
179+
method: 'GET',
180+
})
181+
);
182+
});
183+
184+
it('should call entries on DataTableFormDataBuilder', () => {
185+
expect(dataTableFormDataBuilder.entries).toHaveBeenCalled();
186+
});
187+
188+
it('should return value from api', () => {
189+
expect(result).toEqual(
190+
jasmine.objectContaining({
191+
rows: response.rows,
192+
pageData: response.pageData,
193+
})
194+
);
195+
});
196+
197+
describe('with extraParams', () => {
198+
it('should merge extraParams into the query string', async () => {
199+
const extraParams = {
200+
crashCountStartDate: '2024-01-01',
201+
crashCountEndDate: '2024-12-31'
202+
};
203+
204+
await service.getData({ database }, extraParams);
205+
206+
const mergedParams = { ...formData, ...extraParams };
207+
const expectedQueryString = new URLSearchParams(mergedParams).toString();
208+
expect(bugSplatApiClient.fetch).toHaveBeenCalledWith(
209+
`${url}?${expectedQueryString}`,
210+
jasmine.objectContaining({
211+
method: 'GET',
212+
})
213+
);
214+
});
215+
});
216+
217+
describe('without extraParams', () => {
218+
it('should use only the base form data params in the query string', async () => {
219+
await service.getData({ database });
220+
221+
const expectedQueryString = new URLSearchParams(formData).toString();
222+
expect(bugSplatApiClient.fetch).toHaveBeenCalledWith(
223+
`${url}?${expectedQueryString}`,
224+
jasmine.objectContaining({
225+
method: 'GET',
226+
})
227+
);
228+
});
229+
});
230+
});
158231
});

src/common/data/table-data/table-data-client/table-data-client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export class TableDataClient {
3131
}
3232

3333
async getData<T, U = Record<string, unknown> | undefined>(
34-
request: TableDataRequest
34+
request: TableDataRequest,
35+
extraParams?: Record<string, string>
3536
): Promise<BugSplatResponse<TableDataResponse<T, U>> | BugSplatResponse<ErrorResponse>> {
3637
const factory = () => this._apiClient.createFormData();
3738
const formData = new TableDataFormDataBuilder(factory)
@@ -43,6 +44,11 @@ export class TableDataClient {
4344
.withSortColumn(request.sortColumn)
4445
.withSortOrder(request.sortOrder)
4546
.entries();
47+
48+
if (extraParams) {
49+
Object.assign(formData, extraParams);
50+
}
51+
4652
const requestInit = {
4753
method: 'GET',
4854
cache: 'no-cache',

src/versions/versions-api-client/versions-api-client.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,33 @@ describe('VersionsApiClient', () => {
7474
])
7575
);
7676
});
77+
78+
describe('with crash count date options', () => {
79+
it('should call getData with extraParams when both dates are provided', async () => {
80+
const options = {
81+
crashCountStartDate: '2024-01-01',
82+
crashCountEndDate: '2024-12-31'
83+
};
84+
85+
await versionsApiClient.getVersions({ database }, options);
86+
87+
expect(tableDataClient.getData).toHaveBeenCalledWith(
88+
jasmine.objectContaining({ database }),
89+
jasmine.objectContaining({
90+
crashCountStartDate: '2024-01-01',
91+
crashCountEndDate: '2024-12-31'
92+
})
93+
);
94+
});
95+
96+
it('should call getData without extraParams when no options are provided', async () => {
97+
await versionsApiClient.getVersions({ database });
98+
99+
expect(tableDataClient.getData).toHaveBeenCalledWith(
100+
jasmine.objectContaining({ database })
101+
);
102+
});
103+
});
77104
});
78105

79106
describe('deleteVersions', () => {

src/versions/versions-api-client/versions-api-client.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,23 @@ export class VersionsApiClient {
2323
this._tableDataClient = new TableDataClient(this._client, this.route);
2424
}
2525

26-
async getVersions(request: TableDataRequest): Promise<TableDataResponse<VersionsApiRow>> {
27-
const response = await this._tableDataClient.getData<VersionsApiResponseRow>(request);
26+
async getVersions(
27+
request: TableDataRequest,
28+
options?: { crashCountStartDate?: string; crashCountEndDate?: string }
29+
): Promise<TableDataResponse<VersionsApiRow>> {
30+
const extraParams: Record<string, string> = {};
31+
if (options?.crashCountStartDate) {
32+
extraParams.crashCountStartDate = options.crashCountStartDate;
33+
}
34+
if (options?.crashCountEndDate) {
35+
extraParams.crashCountEndDate = options.crashCountEndDate;
36+
}
37+
38+
const hasExtraParams = Object.keys(extraParams).length > 0;
39+
const response = hasExtraParams
40+
? await this._tableDataClient.getData<VersionsApiResponseRow>(request, extraParams)
41+
: await this._tableDataClient.getData<VersionsApiResponseRow>(request);
42+
2843
if (isErrorResponse(response)) {
2944
throw new Error((await response.json()).message);
3045
}

src/versions/versions-api-row/version-api-row.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,36 @@ describe('VersionApiRow', () => {
3232

3333
expect(result.retired).toEqual(false);
3434
});
35+
36+
it('should convert totalCrashCount string to number', () => {
37+
const row = { totalCrashCount: '100' };
38+
39+
const result = new VersionsApiRow(row as any);
40+
41+
expect(result.totalCrashCount).toEqual(100);
42+
});
43+
44+
it('should convert periodCrashCount string to number', () => {
45+
const row = { periodCrashCount: '50' };
46+
47+
const result = new VersionsApiRow(row as any);
48+
49+
expect(result.periodCrashCount).toEqual(50);
50+
});
51+
52+
it('should default totalCrashCount to 0 when undefined', () => {
53+
const row = {};
54+
55+
const result = new VersionsApiRow(row as any);
56+
57+
expect(result.totalCrashCount).toEqual(0);
58+
});
59+
60+
it('should default periodCrashCount to 0 when undefined', () => {
61+
const row = {};
62+
63+
const result = new VersionsApiRow(row as any);
64+
65+
expect(result.periodCrashCount).toEqual(0);
66+
});
3567
});

src/versions/versions-api-row/versions-api-row.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface VersionsApiResponseRow {
1212
rejectedCount: string;
1313
retired: '0' | '1';
1414
fullDumps: '0' | '1';
15+
totalCrashCount: string;
16+
periodCrashCount: string;
1517
}
1618

1719
export class VersionsApiRow {
@@ -26,6 +28,8 @@ export class VersionsApiRow {
2628
rejectedCount: number;
2729
retired: boolean;
2830
fullDumps: boolean;
31+
totalCrashCount: number;
32+
periodCrashCount: number;
2933

3034
constructor(rawApiRow: VersionsApiResponseRow) {
3135
ac.assertType(rawApiRow, Object, 'rawApiRow');
@@ -52,6 +56,8 @@ export class VersionsApiRow {
5256
this.rejectedCount = Number(rawApiRow.rejectedCount);
5357
this.retired = Boolean(Number(rawApiRow.retired));
5458
this.fullDumps = Boolean(Number(rawApiRow.fullDumps));
59+
this.totalCrashCount = Number(rawApiRow.totalCrashCount ?? 0);
60+
this.periodCrashCount = Number(rawApiRow.periodCrashCount ?? 0);
5561

5662
Object.freeze(this);
5763
}

0 commit comments

Comments
 (0)