Skip to content

Commit 955048f

Browse files
committed
feat(feedback): Add captureFeedback method
1 parent 4759d4c commit 955048f

File tree

11 files changed

+117
-112
lines changed

11 files changed

+117
-112
lines changed

packages/browser/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
8686

8787
/**
8888
* Sends user feedback to Sentry.
89+
*
90+
* @deprecated Use `captureFeedback` instead.
8991
*/
9092
public captureUserFeedback(feedback: UserFeedback): void {
9193
if (!this._isEnabled()) {

packages/browser/src/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export {
8888
init,
8989
onLoad,
9090
showReportDialog,
91+
// eslint-disable-next-line deprecation/deprecation
9192
captureUserFeedback,
9293
} from './sdk';
9394

packages/browser/src/index.bundle.feedback.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export {
1414
feedbackIntegration,
1515
getFeedback,
1616
};
17-
// Note: We do not export a shim for `Span` here, as that is quite complex and would blow up the bundle
17+
18+
export { captureFeedback } from '@sentry/core';

packages/browser/src/index.bundle.tracing.replay.feedback.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {
1919
withActiveSpan,
2020
getSpanDescendants,
2121
setMeasurement,
22+
captureFeedback,
2223
} from '@sentry/core';
2324

2425
export {

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {
1010
extraErrorDataIntegration,
1111
rewriteFramesIntegration,
1212
sessionTimingIntegration,
13+
captureFeedback,
1314
} from '@sentry/core';
1415

1516
export {

packages/browser/src/sdk.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,13 @@ function startSessionTracking(): void {
295295

296296
/**
297297
* Captures user feedback and sends it to Sentry.
298+
*
299+
* @deprecated Use `captureFeedback` instead.
298300
*/
299301
export function captureUserFeedback(feedback: UserFeedback): void {
300302
const client = getClient<BrowserClient>();
301303
if (client) {
304+
// eslint-disable-next-line deprecation/deprecation
302305
client.captureUserFeedback(feedback);
303306
}
304307
}

packages/core/src/feedback.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { Attachment, EventHint, FeedbackEvent } from '@sentry/types';
2+
import { getClient, getCurrentScope } from './currentScopes';
3+
import { createAttachmentEnvelope } from './envelope';
4+
5+
interface FeedbackParams {
6+
message: string;
7+
name?: string;
8+
email?: string;
9+
attachments?: Attachment[];
10+
url?: string;
11+
source?: string;
12+
relatedEventId?: string;
13+
}
14+
15+
/**
16+
* Send user feedback to Sentry.
17+
*/
18+
export function captureFeedback(
19+
feedbackParams: FeedbackParams,
20+
hint?: EventHint & { includeReplay?: boolean },
21+
): string {
22+
const { message, name, email, url, source, attachments } = feedbackParams;
23+
24+
const client = getClient();
25+
const transport = client && client.getTransport();
26+
const dsn = client && client.getDsn();
27+
28+
if (!client || !transport || !dsn) {
29+
throw new Error('Invalid Sentry client');
30+
}
31+
32+
const feedbackEvent: FeedbackEvent = {
33+
contexts: {
34+
feedback: {
35+
contact_email: email,
36+
name,
37+
message,
38+
url,
39+
source,
40+
},
41+
},
42+
type: 'feedback',
43+
level: 'info',
44+
};
45+
46+
// TODO: What to do with `relatedEventId` ?
47+
48+
if (client) {
49+
client.emit('beforeSendFeedback', feedbackEvent, hint);
50+
}
51+
52+
const eventId = getCurrentScope().captureEvent(feedbackEvent, hint);
53+
54+
// For now, we have to send attachments manually in a separate envelope
55+
// Because we do not support attachments in the feedback envelope
56+
// Once the Sentry API properly supports this, we can get rid of this and send it through the event envelope
57+
if (client && attachments && attachments.length) {
58+
const transport = client.getTransport();
59+
const dsn = client.getDsn();
60+
61+
if (dsn && transport) {
62+
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
63+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
64+
void transport.send(
65+
createAttachmentEnvelope(
66+
feedbackEvent,
67+
attachments,
68+
dsn,
69+
client.getOptions()._metadata,
70+
client.getOptions().tunnel,
71+
),
72+
);
73+
}
74+
}
75+
76+
return eventId;
77+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ export { BrowserMetricsAggregator } from './metrics/browser-aggregator';
106106
export { getMetricSummaryJsonForSpan } from './metrics/metric-summary';
107107
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch';
108108
export { trpcMiddleware } from './trpc';
109+
export { captureFeedback } from './feedback';

packages/feedback/src/core/sendFeedback.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,34 @@ describe('sendFeedback', () => {
1414
message: 'mi',
1515
});
1616
expect(mockTransport).toHaveBeenCalledWith([
17-
{ event_id: expect.any(String), sent_at: expect.any(String) },
17+
{
18+
event_id: expect.any(String),
19+
sent_at: expect.any(String),
20+
trace: expect.anything(),
21+
},
1822
[
1923
[
2024
{ type: 'feedback' },
2125
{
2226
breadcrumbs: undefined,
2327
contexts: {
28+
trace: {
29+
parent_span_id: undefined,
30+
span_id: expect.any(String),
31+
trace_id: expect.any(String),
32+
},
2433
feedback: {
2534
contact_email: 're@example.org',
2635
message: 'mi',
2736
name: 'doe',
28-
replay_id: undefined,
2937
source: 'api',
3038
url: 'http://localhost/',
3139
},
3240
},
3341
level: 'info',
3442
environment: 'production',
3543
event_id: expect.any(String),
36-
platform: 'javascript',
44+
// TODO: Why is there no platform here?
3745
timestamp: expect.any(Number),
3846
type: 'feedback',
3947
},

packages/feedback/src/core/sendFeedback.ts

Lines changed: 18 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -10,91 +10,44 @@ import { prepareFeedbackEvent } from '../util/prepareFeedbackEvent';
1010
export const sendFeedback: SendFeedback = (
1111
{ name, email, message, attachments, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
1212
{ includeReplay = true } = {},
13-
) => {
13+
): Promise<void> {
1414
if (!message) {
1515
throw new Error('Unable to submit feedback with empty message');
1616
}
1717

18+
// We want to wait for the feedback to be sent (or not)
1819
const client = getClient();
19-
const transport = client && client.getTransport();
20-
const dsn = client && client.getDsn();
2120

22-
if (!client || !transport || !dsn) {
23-
throw new Error('Invalid Sentry client');
21+
if (!client) {
22+
throw new Error('No client setup, cannot send feedback.');
2423
}
2524

26-
const baseEvent: FeedbackEvent = {
27-
contexts: {
28-
feedback: {
29-
contact_email: email,
30-
name,
31-
message,
32-
url,
33-
source,
34-
},
35-
},
36-
type: 'feedback',
37-
};
25+
const eventId = captureFeedback({ name, email, message, attachments, source, url }, hint);
3826

39-
return withScope(async scope => {
40-
// No use for breadcrumbs in feedback
41-
scope.clearBreadcrumbs();
27+
// We want to wait for the feedback to be sent (or not)
28+
return new Promise<void>((resolve, reject) => {
29+
// After 5s, we want to clear anyhow
30+
const timeout = setTimeout(() => reject('timeout'), 5_000);
4231

43-
if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) {
44-
scope.setLevel('info');
45-
}
46-
47-
const feedbackEvent = await prepareFeedbackEvent({
48-
scope,
49-
client,
50-
event: baseEvent,
51-
});
52-
53-
if (client.emit) {
54-
client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) });
55-
}
56-
57-
try {
58-
const response = await transport.send(
59-
createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel),
60-
);
61-
62-
if (attachments && attachments.length) {
63-
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
64-
await transport.send(
65-
createAttachmentEnvelope(
66-
feedbackEvent,
67-
attachments,
68-
dsn,
69-
client.getOptions()._metadata,
70-
client.getOptions().tunnel,
71-
),
72-
);
32+
client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
33+
if (event.event_id !== eventId) {
34+
return;
7335
}
7436

37+
clearTimeout(timeout);
38+
7539
// Require valid status codes, otherwise can assume feedback was not sent successfully
7640
if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
7741
if (response.statusCode === 0) {
78-
throw new Error(
42+
return reject(
7943
'Unable to send Feedback. This is because of network issues, or because you are using an ad-blocker.',
8044
);
8145
}
82-
throw new Error('Unable to send Feedback. Invalid response from server.');
46+
return reject('Unable to send Feedback. Invalid response from server.');
8347
}
8448

85-
return response;
86-
} catch (err) {
87-
const error = new Error('Unable to send Feedback');
88-
89-
try {
90-
// In case browsers don't allow this property to be writable
91-
// @ts-expect-error This needs lib es2022 and newer
92-
error.cause = err;
93-
} catch {
94-
// nothing to do
95-
}
96-
throw error;
97-
}
49+
resolve();
50+
});
9851
});
9952
};
10053

packages/feedback/src/util/prepareFeedbackEvent.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)