Skip to content

Commit a50c684

Browse files
bobbyg603claude
andauthored
feat: add UserFeedback crash type and feedback submission helper (#162)
* feat: add UserFeedback crash type and feedback submission helper Add CrashType.userFeedback, CrashTypeId.userFeedback enum value, and a new postUserFeedback() helper that constructs XML callstack from a title and submits via the existing crash post pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: better user feedback report xml * fix: use feedback instead of report * fix: rename upload file to feedback.xml and add attributes option Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: switch to feedback.json with JSON.stringify for escaping Replace hand-rolled XML builder and escapeXml() with JSON.stringify(), which handles all escaping natively. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5631faa commit a50c684

File tree

5 files changed

+84
-1
lines changed

5 files changed

+84
-1
lines changed

src/crashes/crashes-api-row/crashes-api-row.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export enum CrashTypeId {
6767
ps4 = 28,
6868
ps5 = 29,
6969
playStationRecap = 30,
70+
userFeedback = 36,
7071
}
7172

7273
export class CrashesApiRow implements CrashDataWithMappedProperties {

src/post/crash-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export class CrashType {
77
static readonly ps4 = new CrashType('PlayStation 4', 28);
88
static readonly ps5 = new CrashType('PlayStation 5', 29);
99
static readonly xml = new CrashType('Xml.Report', 21);
10+
static readonly userFeedback = new CrashType('User.Feedback', 36);
1011
private constructor(public readonly name: string, public readonly id: number) { }
1112
}

src/post/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { CrashType } from './crash-type';
2-
export { CrashPostClient } from './crash-post-client';
2+
export { CrashPostClient } from './crash-post-client';
3+
export { buildFeedbackJson, postUserFeedback } from './user-feedback';
4+
export type { UserFeedbackOptions } from './user-feedback';

src/post/user-feedback.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { buildFeedbackJson } from './user-feedback';
2+
3+
describe('buildFeedbackJson', () => {
4+
it('should produce JSON with title and description', () => {
5+
const json = buildFeedbackJson('Login button broken', 'Nothing happens when I tap login');
6+
const parsed = JSON.parse(json);
7+
8+
expect(parsed.title).toBe('Login button broken');
9+
expect(parsed.description).toBe('Nothing happens when I tap login');
10+
});
11+
12+
it('should handle empty description', () => {
13+
const json = buildFeedbackJson('Crash on startup');
14+
const parsed = JSON.parse(json);
15+
16+
expect(parsed.title).toBe('Crash on startup');
17+
expect(parsed.description).toBe('');
18+
});
19+
20+
it('should handle special characters without manual escaping', () => {
21+
const json = buildFeedbackJson('Error <"test"> & \'more\'', 'Use <b>bold</b> & "quotes"');
22+
const parsed = JSON.parse(json);
23+
24+
expect(parsed.title).toBe('Error <"test"> & \'more\'');
25+
expect(parsed.description).toBe('Use <b>bold</b> & "quotes"');
26+
});
27+
28+
it('should produce valid JSON', () => {
29+
const json = buildFeedbackJson('Test', 'Desc');
30+
31+
expect(() => JSON.parse(json)).not.toThrow();
32+
});
33+
});

src/post/user-feedback.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { UploadableFile } from '@common';
2+
import { CrashPostClient } from './crash-post-client';
3+
import { CrashType } from './crash-type';
4+
5+
export interface UserFeedbackOptions {
6+
title: string;
7+
description?: string;
8+
user?: string;
9+
email?: string;
10+
attachments?: UploadableFile[];
11+
attributes?: Record<string, string>;
12+
}
13+
14+
export function buildFeedbackJson(title: string, description?: string): string {
15+
return JSON.stringify({
16+
title,
17+
description: description ?? '',
18+
});
19+
}
20+
21+
export async function postUserFeedback(
22+
client: CrashPostClient,
23+
application: string,
24+
version: string,
25+
options: UserFeedbackOptions,
26+
): Promise<ReturnType<CrashPostClient['postCrash']>> {
27+
const json = buildFeedbackJson(options.title, options.description);
28+
const jsonBuffer = Buffer.from(json, 'utf-8');
29+
const jsonFile = new UploadableFile('feedback.json', jsonBuffer.length, jsonBuffer);
30+
31+
const attributes: Record<string, string> = { ...options.attributes };
32+
if (options.user) {
33+
attributes['user'] = options.user;
34+
}
35+
if (options.email) {
36+
attributes['email'] = options.email;
37+
}
38+
39+
return client.postCrash(
40+
application,
41+
version,
42+
CrashType.userFeedback,
43+
jsonFile,
44+
Object.keys(attributes).length > 0 ? attributes : undefined
45+
);
46+
}

0 commit comments

Comments
 (0)