Skip to content

Commit

Permalink
feat(feedback): Add captureFeedback method
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Apr 9, 2024
1 parent 4759d4c commit 955048f
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 112 deletions.
2 changes: 2 additions & 0 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {

/**
* Sends user feedback to Sentry.
*
* @deprecated Use `captureFeedback` instead.
*/
public captureUserFeedback(feedback: UserFeedback): void {
if (!this._isEnabled()) {
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export {
init,
onLoad,
showReportDialog,
// eslint-disable-next-line deprecation/deprecation
captureUserFeedback,
} from './sdk';

Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/index.bundle.feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export {
feedbackIntegration,
getFeedback,
};
// Note: We do not export a shim for `Span` here, as that is quite complex and would blow up the bundle

export { captureFeedback } from '@sentry/core';
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
withActiveSpan,
getSpanDescendants,
setMeasurement,
captureFeedback,
} from '@sentry/core';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
extraErrorDataIntegration,
rewriteFramesIntegration,
sessionTimingIntegration,
captureFeedback,
} from '@sentry/core';

export {
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,13 @@ function startSessionTracking(): void {

/**
* Captures user feedback and sends it to Sentry.
*
* @deprecated Use `captureFeedback` instead.
*/
export function captureUserFeedback(feedback: UserFeedback): void {
const client = getClient<BrowserClient>();
if (client) {
// eslint-disable-next-line deprecation/deprecation
client.captureUserFeedback(feedback);
}
}
77 changes: 77 additions & 0 deletions packages/core/src/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Attachment, EventHint, FeedbackEvent } from '@sentry/types';
import { getClient, getCurrentScope } from './currentScopes';
import { createAttachmentEnvelope } from './envelope';

interface FeedbackParams {
message: string;
name?: string;
email?: string;
attachments?: Attachment[];
url?: string;
source?: string;
relatedEventId?: string;
}

/**
* Send user feedback to Sentry.
*/
export function captureFeedback(
feedbackParams: FeedbackParams,
hint?: EventHint & { includeReplay?: boolean },
): string {
const { message, name, email, url, source, attachments } = feedbackParams;

const client = getClient();
const transport = client && client.getTransport();
const dsn = client && client.getDsn();

if (!client || !transport || !dsn) {
throw new Error('Invalid Sentry client');
}

const feedbackEvent: FeedbackEvent = {
contexts: {
feedback: {
contact_email: email,
name,
message,
url,
source,
},
},
type: 'feedback',
level: 'info',
};

// TODO: What to do with `relatedEventId` ?

if (client) {
client.emit('beforeSendFeedback', feedbackEvent, hint);
}

const eventId = getCurrentScope().captureEvent(feedbackEvent, hint);

// For now, we have to send attachments manually in a separate envelope
// Because we do not support attachments in the feedback envelope
// Once the Sentry API properly supports this, we can get rid of this and send it through the event envelope
if (client && attachments && attachments.length) {
const transport = client.getTransport();
const dsn = client.getDsn();

if (dsn && transport) {
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
// eslint-disable-next-line @typescript-eslint/no-floating-promises
void transport.send(
createAttachmentEnvelope(
feedbackEvent,
attachments,
dsn,
client.getOptions()._metadata,
client.getOptions().tunnel,
),
);
}
}

return eventId;
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ export { BrowserMetricsAggregator } from './metrics/browser-aggregator';
export { getMetricSummaryJsonForSpan } from './metrics/metric-summary';
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch';
export { trpcMiddleware } from './trpc';
export { captureFeedback } from './feedback';
14 changes: 11 additions & 3 deletions packages/feedback/src/core/sendFeedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,34 @@ describe('sendFeedback', () => {
message: 'mi',
});
expect(mockTransport).toHaveBeenCalledWith([
{ event_id: expect.any(String), sent_at: expect.any(String) },
{
event_id: expect.any(String),
sent_at: expect.any(String),
trace: expect.anything(),
},
[
[
{ type: 'feedback' },
{
breadcrumbs: undefined,
contexts: {
trace: {
parent_span_id: undefined,
span_id: expect.any(String),
trace_id: expect.any(String),
},
feedback: {
contact_email: 're@example.org',
message: 'mi',
name: 'doe',
replay_id: undefined,
source: 'api',
url: 'http://localhost/',
},
},
level: 'info',
environment: 'production',
event_id: expect.any(String),
platform: 'javascript',
// TODO: Why is there no platform here?
timestamp: expect.any(Number),
type: 'feedback',
},
Expand Down
83 changes: 18 additions & 65 deletions packages/feedback/src/core/sendFeedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,91 +10,44 @@ import { prepareFeedbackEvent } from '../util/prepareFeedbackEvent';
export const sendFeedback: SendFeedback = (
{ name, email, message, attachments, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
{ includeReplay = true } = {},
) => {
): Promise<void> {
if (!message) {
throw new Error('Unable to submit feedback with empty message');
}

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

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

const baseEvent: FeedbackEvent = {
contexts: {
feedback: {
contact_email: email,
name,
message,
url,
source,
},
},
type: 'feedback',
};
const eventId = captureFeedback({ name, email, message, attachments, source, url }, hint);

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

if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) {
scope.setLevel('info');
}

const feedbackEvent = await prepareFeedbackEvent({
scope,
client,
event: baseEvent,
});

if (client.emit) {
client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) });
}

try {
const response = await transport.send(
createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel),
);

if (attachments && attachments.length) {
// TODO: https://docs.sentry.io/platforms/javascript/enriching-events/attachments/
await transport.send(
createAttachmentEnvelope(
feedbackEvent,
attachments,
dsn,
client.getOptions()._metadata,
client.getOptions().tunnel,
),
);
client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
if (event.event_id !== eventId) {
return;
}

clearTimeout(timeout);

// Require valid status codes, otherwise can assume feedback was not sent successfully
if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
if (response.statusCode === 0) {
throw new Error(
return reject(
'Unable to send Feedback. This is because of network issues, or because you are using an ad-blocker.',
);
}
throw new Error('Unable to send Feedback. Invalid response from server.');
return reject('Unable to send Feedback. Invalid response from server.');
}

return response;
} catch (err) {
const error = new Error('Unable to send Feedback');

try {
// In case browsers don't allow this property to be writable
// @ts-expect-error This needs lib es2022 and newer
error.cause = err;
} catch {
// nothing to do
}
throw error;
}
resolve();
});
});
};

Expand Down
43 changes: 0 additions & 43 deletions packages/feedback/src/util/prepareFeedbackEvent.ts

This file was deleted.

0 comments on commit 955048f

Please sign in to comment.