Skip to content

Commit dfab71c

Browse files
committed
feat(feedback): Add captureFeedback method
1 parent 3790f1b commit dfab71c

12 files changed

+123
-117
lines changed

packages/browser/src/client.ts

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

8484
/**
8585
* Sends user feedback to Sentry.
86+
*
87+
* @deprecated Use `captureFeedback` instead.
8688
*/
8789
public captureUserFeedback(feedback: UserFeedback): void {
8890
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
@@ -300,10 +300,13 @@ function startSessionTracking(): void {
300300

301301
/**
302302
* Captures user feedback and sends it to Sentry.
303+
*
304+
* @deprecated Use `captureFeedback` instead.
303305
*/
304306
export function captureUserFeedback(feedback: UserFeedback): void {
305307
const client = getClient<BrowserClient>();
306308
if (client) {
309+
// eslint-disable-next-line deprecation/deprecation
307310
client.captureUserFeedback(feedback);
308311
}
309312
}

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: '[email protected]',
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: 22 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,53 @@
1-
import { createAttachmentEnvelope, createEventEnvelope, getClient, withScope } from '@sentry/core';
2-
import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
1+
import { captureFeedback, getClient } from '@sentry/core';
2+
import type { Event, TransportMakeRequestResponse } from '@sentry/types';
33
import { getLocationHref } from '@sentry/utils';
4-
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
4+
import { FEEDBACK_API_SOURCE } from '../constants';
55
import type { SendFeedbackOptions, SendFeedbackParams } from '../types';
6-
import { prepareFeedbackEvent } from '../util/prepareFeedbackEvent';
76

87
/**
98
* Public API to send a Feedback item to Sentry
109
*/
1110
export async function sendFeedback(
1211
{ name, email, message, attachments, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
13-
{ includeReplay = true }: SendFeedbackOptions = {},
14-
): Promise<TransportMakeRequestResponse> {
12+
hint?: SendFeedbackOptions,
13+
): Promise<void> {
1514
if (!message) {
1615
throw new Error('Unable to submit feedback with empty message');
1716
}
1817

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

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

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

40-
return withScope(async scope => {
41-
// No use for breadcrumbs in feedback
42-
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);
4331

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

37+
clearTimeout(timeout);
38+
7639
// Require valid status codes, otherwise can assume feedback was not sent successfully
7740
if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
7841
if (response.statusCode === 0) {
79-
throw new Error(
42+
return reject(
8043
'Unable to send Feedback. This is because of network issues, or because you are using an ad-blocker.',
8144
);
8245
}
83-
throw new Error('Unable to send Feedback. Invalid response from server.');
46+
return reject('Unable to send Feedback. Invalid response from server.');
8447
}
8548

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

packages/feedback/src/util/prepareFeedbackEvent.ts

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

yarn.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6647,6 +6647,7 @@
66476647
"@types/unist" "*"
66486648

66496649
"@types/history-4@npm:@types/[email protected]", "@types/history-5@npm:@types/[email protected]", "@types/history@*":
6650+
name "@types/history-4"
66506651
version "4.7.8"
66516652
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934"
66526653
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
@@ -25275,6 +25276,7 @@ react-is@^18.0.0:
2527525276
"@remix-run/router" "1.0.2"
2527625277

2527725278
"react-router-6@npm:[email protected]", [email protected]:
25279+
name react-router-6
2527825280
version "6.3.0"
2527925281
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557"
2528025282
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==

0 commit comments

Comments
 (0)