Skip to content

Commit 8570dd6

Browse files
committed
feat(feedback): Add captureFeedback method
fix PR
1 parent 3a45a3c commit 8570dd6

12 files changed

+118
-120
lines changed

packages/browser/src/client.ts

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

9292
/**
9393
* Sends user feedback to Sentry.
94+
*
95+
* @deprecated Use `captureFeedback` instead.
9496
*/
9597
public captureUserFeedback(feedback: UserFeedback): void {
9698
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export {
1010
} from '@sentry-internal/feedback';
1111

1212
export { browserTracingIntegrationShim as browserTracingIntegration, replayIntegrationShim as replayIntegration };
13+
14+
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
@@ -13,6 +13,7 @@ export {
1313
withActiveSpan,
1414
getSpanDescendants,
1515
setMeasurement,
16+
captureFeedback,
1617
} from '@sentry/core';
1718

1819
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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
associatedEventId?: 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, associatedEventId } = 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+
associated_event_id: associatedEventId,
41+
},
42+
},
43+
type: 'feedback',
44+
level: 'info',
45+
};
46+
47+
if (client) {
48+
client.emit('beforeSendFeedback', feedbackEvent, hint);
49+
}
50+
51+
const eventId = getCurrentScope().captureEvent(feedbackEvent, hint);
52+
53+
// For now, we have to send attachments manually in a separate envelope
54+
// Because we do not support attachments in the feedback envelope
55+
// Once the Sentry API properly supports this, we can get rid of this and send it through the event envelope
56+
if (client && attachments && attachments.length) {
57+
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
58+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
59+
void transport.send(
60+
createAttachmentEnvelope(
61+
feedbackEvent,
62+
attachments,
63+
dsn,
64+
client.getOptions()._metadata,
65+
client.getOptions().tunnel,
66+
),
67+
);
68+
}
69+
70+
return eventId;
71+
}

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export { BrowserMetricsAggregator } from './metrics/browser-aggregator';
9999
export { getMetricSummaryJsonForSpan } from './metrics/metric-summary';
100100
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch';
101101
export { trpcMiddleware } from './trpc';
102+
export { captureFeedback } from './feedback';
102103

103104
// eslint-disable-next-line deprecation/deprecation
104105
export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim';

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: 23 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,54 @@
1-
import { createAttachmentEnvelope, createEventEnvelope, getClient, withScope } from '@sentry/core';
2-
import type { FeedbackEvent, SendFeedback, SendFeedbackParams } from '@sentry/types';
1+
import { captureFeedback } from '@sentry/core';
2+
import { getClient } from '@sentry/core';
3+
import type { SendFeedback, SendFeedbackParams, TransportMakeRequestResponse } from '@sentry/types';
4+
import type { Event } from '@sentry/types';
35
import { getLocationHref } from '@sentry/utils';
4-
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
5-
import { prepareFeedbackEvent } from '../util/prepareFeedbackEvent';
6+
import { FEEDBACK_API_SOURCE } from '../constants';
67

78
/**
89
* Public API to send a Feedback item to Sentry
910
*/
1011
export const sendFeedback: SendFeedback = (
1112
{ name, email, message, attachments, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
1213
{ includeReplay = true } = {},
13-
) => {
14+
): Promise<void> => {
1415
if (!message) {
1516
throw new Error('Unable to submit feedback with empty message');
1617
}
1718

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

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

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-
};
26+
const eventId = captureFeedback({ name, email, message, attachments, source, url }, { includeReplay });
3827

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

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-
);
33+
client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
34+
if (event.event_id !== eventId) {
35+
return;
7336
}
7437

38+
clearTimeout(timeout);
39+
7540
// Require valid status codes, otherwise can assume feedback was not sent successfully
7641
if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
7742
if (response.statusCode === 0) {
78-
throw new Error(
43+
return reject(
7944
'Unable to send Feedback. This is because of network issues, or because you are using an ad-blocker.',
8045
);
8146
}
82-
throw new Error('Unable to send Feedback. Invalid response from server.');
47+
return reject('Unable to send Feedback. Invalid response from server.');
8348
}
8449

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-
}
50+
resolve();
51+
});
9852
});
9953
};
10054

packages/feedback/src/util/prepareFeedbackEvent.ts

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

packages/types/src/feedback/sendFeedback.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Attachment } from '../attachment';
22
import type { Event } from '../event';
3-
import type { TransportMakeRequestResponse } from '../transport';
43
import type { User } from '../user';
54

65
/**
@@ -19,6 +18,7 @@ interface FeedbackContext extends Record<string, unknown> {
1918
name?: string;
2019
replay_id?: string;
2120
url?: string;
21+
associated_event_id?: string;
2222
}
2323

2424
/**
@@ -48,7 +48,4 @@ interface SendFeedbackOptions {
4848
includeReplay?: boolean;
4949
}
5050

51-
export type SendFeedback = (
52-
params: SendFeedbackParams,
53-
options?: SendFeedbackOptions,
54-
) => Promise<TransportMakeRequestResponse>;
51+
export type SendFeedback = (params: SendFeedbackParams, options?: SendFeedbackOptions) => Promise<void>;

0 commit comments

Comments
 (0)