Skip to content

Commit e5863cd

Browse files
committed
ref(feedback): Refactor attaching replay id to feedback event to use hooks
Add a `afterPrepareFeedback` hook that replay listens to, to attach replayId to the feedback event.
1 parent 13aaf3c commit e5863cd

File tree

6 files changed

+61
-24
lines changed

6 files changed

+61
-24
lines changed

packages/core/src/baseclient.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
Transport,
2828
TransportMakeRequestResponse,
2929
} from '@sentry/types';
30+
import type { FeedbackEvent } from '@sentry/types';
3031
import {
3132
addItemToEnvelope,
3233
checkOrSetAlreadyCaught,
@@ -413,6 +414,12 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
413414
/** @inheritdoc */
414415
public on(hook: 'otelSpanEnd', callback: (otelSpan: unknown, mutableOptions: { drop: boolean }) => void): void;
415416

417+
/** @inheritdoc */
418+
public on(
419+
hook: 'afterPrepareFeedback',
420+
callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void,
421+
): void;
422+
416423
/** @inheritdoc */
417424
public on(hook: string, callback: unknown): void {
418425
if (!this._hooks[hook]) {
@@ -450,6 +457,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
450457
/** @inheritdoc */
451458
public emit(hook: 'otelSpanEnd', otelSpan: unknown, mutableOptions: { drop: boolean }): void;
452459

460+
/** @inheritdoc */
461+
public emit(hook: 'afterPrepareFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void;
462+
453463
/** @inheritdoc */
454464
public emit(hook: string, ...rest: unknown[]): void {
455465
if (this._hooks[hook]) {

packages/feedback/src/sendFeedback.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { BrowserClient, Replay } from '@sentry/browser';
2-
import { getCurrentHub } from '@sentry/core';
31
import { getLocationHref } from '@sentry/utils';
42

53
import { FEEDBACK_API_SOURCE } from './constants';
@@ -19,27 +17,22 @@ interface SendFeedbackParams {
1917
*/
2018
export function sendFeedback(
2119
{ name, email, message, source = FEEDBACK_API_SOURCE, url = getLocationHref() }: SendFeedbackParams,
22-
{ includeReplay = true }: SendFeedbackOptions = {},
20+
options: SendFeedbackOptions = {},
2321
): ReturnType<typeof sendFeedbackRequest> {
24-
const client = getCurrentHub().getClient<BrowserClient>();
25-
const replay = includeReplay && client ? (client.getIntegrationById('Replay') as Replay | undefined) : undefined;
26-
27-
// Prepare session replay
28-
replay && replay.flush();
29-
const replayId = replay && replay.getReplayId();
30-
3122
if (!message) {
3223
throw new Error('Unable to submit feedback with empty message');
3324
}
3425

35-
return sendFeedbackRequest({
36-
feedback: {
37-
name,
38-
email,
39-
message,
40-
url,
41-
replay_id: replayId,
42-
source,
26+
return sendFeedbackRequest(
27+
{
28+
feedback: {
29+
name,
30+
email,
31+
message,
32+
url,
33+
source,
34+
},
4335
},
44-
});
36+
options,
37+
);
4538
}

packages/feedback/src/util/prepareFeedbackEvent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export async function prepareFeedbackEvent({
2727
scope,
2828
client,
2929
)) as FeedbackEvent | null;
30+
3031
if (preparedEvent === null) {
3132
// Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
3233
client.recordDroppedEvent('event_processor', 'feedback', event);

packages/feedback/src/util/sendFeedbackRequest.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { createEventEnvelope, getCurrentHub } from '@sentry/core';
22
import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
33

44
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
5-
import type { SendFeedbackData } from '../types';
5+
import type { SendFeedbackData, SendFeedbackOptions } from '../types';
66
import { prepareFeedbackEvent } from './prepareFeedbackEvent';
77

88
/**
99
* Send feedback using transport
1010
*/
11-
export async function sendFeedbackRequest({
12-
feedback: { message, email, name, source, replay_id, url },
13-
}: SendFeedbackData): Promise<void | TransportMakeRequestResponse> {
11+
export async function sendFeedbackRequest(
12+
{ feedback: { message, email, name, source, url } }: SendFeedbackData,
13+
{ includeReplay = true }: SendFeedbackOptions = {},
14+
): Promise<void | TransportMakeRequestResponse> {
1415
const hub = getCurrentHub();
1516
const client = hub.getClient();
1617
const transport = client && client.getTransport();
@@ -26,7 +27,6 @@ export async function sendFeedbackRequest({
2627
contact_email: email,
2728
name,
2829
message,
29-
replay_id,
3030
url,
3131
source,
3232
},
@@ -54,6 +54,10 @@ export async function sendFeedbackRequest({
5454
return;
5555
}
5656

57+
if (client && client.emit) {
58+
client.emit('afterPrepareFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) });
59+
}
60+
5761
const envelope = createEventEnvelope(
5862
feedbackEvent,
5963
dsn,

packages/replay/src/util/addGlobalListeners.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ export function addGlobalListeners(replay: ReplayContainer): void {
5757
client.on('finishTransaction', transaction => {
5858
replay.lastTransaction = transaction;
5959
});
60+
61+
// We want to flush replay
62+
client.on('afterPrepareFeedback', (feedbackEvent, options) => {
63+
const replayId = replay.getSessionId();
64+
if (options && options.includeReplay && replay.isEnabled() && replayId) {
65+
void replay.flush();
66+
if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) {
67+
feedbackEvent.contexts.feedback.replay_id = replayId;
68+
}
69+
}
70+
});
6071
}
6172
}
6273

packages/types/src/client.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { DsnComponents } from './dsn';
66
import type { DynamicSamplingContext, Envelope } from './envelope';
77
import type { Event, EventHint } from './event';
88
import type { EventProcessor } from './eventprocessor';
9+
import type { FeedbackEvent } from './feedback';
910
import type { Integration, IntegrationClass } from './integration';
1011
import type { ClientOptions } from './options';
1112
import type { Scope } from './scope';
@@ -237,6 +238,16 @@ export interface Client<O extends ClientOptions = ClientOptions> {
237238
*/
238239
on?(hook: 'otelSpanEnd', callback: (otelSpan: unknown, mutableOptions: { drop: boolean }) => void): void;
239240

241+
/**
242+
* Register a callback when a Feedback event has been prepared.
243+
* This should be used to mutate the event. The options argument can hint
244+
* about what kind of mutation it expects.
245+
*/
246+
on?(
247+
hook: 'afterPrepareFeedback',
248+
callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void,
249+
): void;
250+
240251
/**
241252
* Fire a hook event for transaction start.
242253
* Expects to be given a transaction as the second argument.
@@ -291,5 +302,12 @@ export interface Client<O extends ClientOptions = ClientOptions> {
291302
*/
292303
emit?(hook: 'otelSpanEnd', otelSpan: unknown, mutableOptions: { drop: boolean }): void;
293304

305+
/**
306+
* Fire a hook event for after preparing a feedback event. Events to be given
307+
* a feedback event as the second argument, and an optional options object as
308+
* third argument.
309+
*/
310+
emit?(hook: 'afterPrepareFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void;
311+
294312
/* eslint-enable @typescript-eslint/unified-signatures */
295313
}

0 commit comments

Comments
 (0)