Skip to content

Commit c90a60f

Browse files
krystofwoldrichLuca Forstner
and
Luca Forstner
authored
feat(browser): Add captureUserFeedback (#7729)
Add new API `captureUserFeedback` to Browser SDKs, allowing Sentry users to send feedback programmatically without opening and using the feedback dialog. Co-authored-by: Luca Forstner <[email protected]>
1 parent 367f779 commit c90a60f

File tree

8 files changed

+183
-1
lines changed

8 files changed

+183
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Sentry.captureUserFeedback({
2+
eventId: 'test_event_id',
3+
email: 'test_email',
4+
comments: 'test_comments',
5+
name: 'test_name',
6+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect } from '@playwright/test';
2+
import type { UserFeedback } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
6+
7+
sentryTest('should capture simple user feedback', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<UserFeedback>(page, url);
11+
12+
expect(eventData).toMatchObject({
13+
eventId: 'test_event_id',
14+
email: 'test_email',
15+
comments: 'test_comments',
16+
name: 'test_name',
17+
});
18+
});

packages/browser/src/client.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Options,
99
Severity,
1010
SeverityLevel,
11+
UserFeedback,
1112
} from '@sentry/types';
1213
import { createClientReportEnvelope, dsnToString, getSDKSource, logger } from '@sentry/utils';
1314

@@ -16,6 +17,7 @@ import { WINDOW } from './helpers';
1617
import type { Breadcrumbs } from './integrations';
1718
import { BREADCRUMB_INTEGRATION_ID } from './integrations/breadcrumbs';
1819
import type { BrowserTransportOptions } from './transports/types';
20+
import { createUserFeedbackEnvelope } from './userfeedback';
1921

2022
/**
2123
* Configuration options for the Sentry Browser SDK.
@@ -106,6 +108,23 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
106108
super.sendEvent(event, hint);
107109
}
108110

111+
/**
112+
* Sends user feedback to Sentry.
113+
*/
114+
public captureUserFeedback(feedback: UserFeedback): void {
115+
if (!this._isEnabled()) {
116+
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture user feedback.');
117+
return;
118+
}
119+
120+
const envelope = createUserFeedbackEnvelope(feedback, {
121+
metadata: this.getSdkMetadata(),
122+
dsn: this.getDsn(),
123+
tunnel: this.getOptions().tunnel,
124+
});
125+
void this._sendEnvelope(envelope);
126+
}
127+
109128
/**
110129
* @inheritDoc
111130
*/

packages/browser/src/exports.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,17 @@ export {
5959
winjsStackLineParser,
6060
} from './stack-parsers';
6161
export { eventFromException, eventFromMessage } from './eventbuilder';
62-
export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk';
62+
export { createUserFeedbackEnvelope } from './userfeedback';
63+
export {
64+
defaultIntegrations,
65+
forceLoad,
66+
init,
67+
lastEventId,
68+
onLoad,
69+
showReportDialog,
70+
flush,
71+
close,
72+
wrap,
73+
captureUserFeedback,
74+
} from './sdk';
6375
export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations';

packages/browser/src/sdk.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
initAndBind,
77
Integrations as CoreIntegrations,
88
} from '@sentry/core';
9+
import type { UserFeedback } from '@sentry/types';
910
import {
1011
addInstrumentationHandler,
1112
logger,
@@ -289,3 +290,13 @@ function startSessionTracking(): void {
289290
}
290291
});
291292
}
293+
294+
/**
295+
* Captures user feedback and sends it to Sentry.
296+
*/
297+
export function captureUserFeedback(feedback: UserFeedback): void {
298+
const client = getCurrentHub().getClient<BrowserClient>();
299+
if (client) {
300+
client.captureUserFeedback(feedback);
301+
}
302+
}

packages/browser/src/userfeedback.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { DsnComponents, EventEnvelope, SdkMetadata, UserFeedback, UserFeedbackItem } from '@sentry/types';
2+
import { createEnvelope, dsnToString } from '@sentry/utils';
3+
4+
/**
5+
* Creates an envelope from a user feedback.
6+
*/
7+
export function createUserFeedbackEnvelope(
8+
feedback: UserFeedback,
9+
{
10+
metadata,
11+
tunnel,
12+
dsn,
13+
}: {
14+
metadata: SdkMetadata | undefined;
15+
tunnel: string | undefined;
16+
dsn: DsnComponents | undefined;
17+
},
18+
): EventEnvelope {
19+
const headers: EventEnvelope[0] = {
20+
event_id: feedback.event_id,
21+
sent_at: new Date().toISOString(),
22+
...(metadata &&
23+
metadata.sdk && {
24+
sdk: {
25+
name: metadata.sdk.name,
26+
version: metadata.sdk.version,
27+
},
28+
}),
29+
...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }),
30+
};
31+
const item = createUserFeedbackEnvelopeItem(feedback);
32+
33+
return createEnvelope(headers, [item]);
34+
}
35+
36+
function createUserFeedbackEnvelopeItem(feedback: UserFeedback): UserFeedbackItem {
37+
const feedbackHeaders: UserFeedbackItem[0] = {
38+
type: 'user_report',
39+
};
40+
return [feedbackHeaders, feedback];
41+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { createUserFeedbackEnvelope } from '../../src/userfeedback';
2+
3+
describe('userFeedback', () => {
4+
test('creates user feedback envelope header', () => {
5+
const envelope = createUserFeedbackEnvelope(
6+
{
7+
comments: 'Test Comments',
8+
9+
name: 'Test User',
10+
event_id: 'testEvent123',
11+
},
12+
{
13+
metadata: {
14+
sdk: {
15+
name: 'testSdkName',
16+
version: 'testSdkVersion',
17+
},
18+
},
19+
tunnel: 'testTunnel',
20+
dsn: {
21+
host: 'testHost',
22+
projectId: 'testProjectId',
23+
protocol: 'http',
24+
},
25+
},
26+
);
27+
28+
expect(envelope[0]).toEqual({
29+
dsn: 'http://undefined@testHost/undefinedtestProjectId',
30+
event_id: 'testEvent123',
31+
sdk: {
32+
name: 'testSdkName',
33+
version: 'testSdkVersion',
34+
},
35+
sent_at: expect.any(String),
36+
});
37+
});
38+
39+
test('creates user feedback envelope item', () => {
40+
const envelope = createUserFeedbackEnvelope(
41+
{
42+
comments: 'Test Comments',
43+
44+
name: 'Test User',
45+
event_id: 'testEvent123',
46+
},
47+
{
48+
metadata: undefined,
49+
tunnel: undefined,
50+
dsn: undefined,
51+
},
52+
);
53+
54+
expect(envelope[1]).toEqual([
55+
[
56+
{
57+
type: 'user_report',
58+
},
59+
{
60+
comments: 'Test Comments',
61+
62+
name: 'Test User',
63+
event_id: 'testEvent123',
64+
},
65+
],
66+
]);
67+
});
68+
});

0 commit comments

Comments
 (0)