Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): Add captureUserFeedback #7729

Merged
merged 10 commits into from
Apr 5, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Sentry.captureUserFeedback({
eventId: 'test_event_id',
email: 'test_email',
comments: 'test_comments',
name: 'test_name',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect } from '@playwright/test';
import type { UserFeedback } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';

sentryTest('should capture simple user feedback', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<UserFeedback>(page, url);

expect(eventData).toMatchObject({
eventId: 'test_event_id',
email: 'test_email',
comments: 'test_comments',
name: 'test_name',
});
});
19 changes: 19 additions & 0 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
Options,
Severity,
SeverityLevel,
UserFeedback,
} from '@sentry/types';
import { createClientReportEnvelope, dsnToString, getSDKSource, logger } from '@sentry/utils';

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

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

/**
* Sends user feedback to Sentry.
*/
public captureUserFeedback(feedback: UserFeedback): void {
if (!this._isEnabled()) {
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture user feedback.');
return;
}

const envelope = createUserFeedbackEnvelope(feedback, {
metadata: this.getSdkMetadata(),
dsn: this.getDsn(),
tunnel: this.getOptions().tunnel,
});
void this._sendEnvelope(envelope);
}

/**
* @inheritDoc
*/
Expand Down
14 changes: 13 additions & 1 deletion packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,17 @@ export {
winjsStackLineParser,
} from './stack-parsers';
export { eventFromException, eventFromMessage } from './eventbuilder';
export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk';
export { createUserFeedbackEnvelope } from './userfeedback';
export {
defaultIntegrations,
forceLoad,
init,
lastEventId,
onLoad,
showReportDialog,
flush,
close,
wrap,
captureUserFeedback,
} from './sdk';
export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations';
11 changes: 11 additions & 0 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
initAndBind,
Integrations as CoreIntegrations,
} from '@sentry/core';
import type { UserFeedback } from '@sentry/types';
import {
addInstrumentationHandler,
logger,
Expand Down Expand Up @@ -289,3 +290,13 @@ function startSessionTracking(): void {
}
});
}

/**
* Captures user feedback and sends it to Sentry.
*/
export function captureUserFeedback(feedback: UserFeedback): void {
const client = getCurrentHub().getClient<BrowserClient>();
if (client) {
client.captureUserFeedback(feedback);
}
}
41 changes: 41 additions & 0 deletions packages/browser/src/userfeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { DsnComponents, EventEnvelope, SdkMetadata, UserFeedback, UserFeedbackItem } from '@sentry/types';
import { createEnvelope, dsnToString } from '@sentry/utils';

/**
* Creates an envelope from a user feedback.
*/
export function createUserFeedbackEnvelope(
feedback: UserFeedback,
{
metadata,
tunnel,
dsn,
}: {
metadata: SdkMetadata | undefined;
tunnel: string | undefined;
dsn: DsnComponents | undefined;
},
): EventEnvelope {
const headers: EventEnvelope[0] = {
event_id: feedback.event_id,
sent_at: new Date().toISOString(),
...(metadata &&
metadata.sdk && {
sdk: {
name: metadata.sdk.name,
version: metadata.sdk.version,
},
}),
...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }),
};
const item = createUserFeedbackEnvelopeItem(feedback);

return createEnvelope(headers, [item]);
}

function createUserFeedbackEnvelopeItem(feedback: UserFeedback): UserFeedbackItem {
const feedbackHeaders: UserFeedbackItem[0] = {
type: 'user_report',
};
return [feedbackHeaders, feedback];
}
68 changes: 68 additions & 0 deletions packages/browser/test/unit/userfeedback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createUserFeedbackEnvelope } from '../../src/userfeedback';

describe('userFeedback', () => {
test('creates user feedback envelope header', () => {
const envelope = createUserFeedbackEnvelope(
{
comments: 'Test Comments',
email: 'test@email.com',
name: 'Test User',
event_id: 'testEvent123',
},
{
metadata: {
sdk: {
name: 'testSdkName',
version: 'testSdkVersion',
},
},
tunnel: 'testTunnel',
dsn: {
host: 'testHost',
projectId: 'testProjectId',
protocol: 'http',
},
},
);

expect(envelope[0]).toEqual({
dsn: 'http://undefined@testHost/undefinedtestProjectId',
event_id: 'testEvent123',
sdk: {
name: 'testSdkName',
version: 'testSdkVersion',
},
sent_at: expect.any(String),
});
});

test('creates user feedback envelope item', () => {
const envelope = createUserFeedbackEnvelope(
{
comments: 'Test Comments',
email: 'test@email.com',
name: 'Test User',
event_id: 'testEvent123',
},
{
metadata: undefined,
tunnel: undefined,
dsn: undefined,
},
);

expect(envelope[1]).toEqual([
[
{
type: 'user_report',
},
{
comments: 'Test Comments',
email: 'test@email.com',
name: 'Test User',
event_id: 'testEvent123',
},
],
]);
});
});