Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core/src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { debug } from '../utils/debug-logger';
import {
createEnvelope,
envelopeContainsItemType,
envelopeItemTypeToDataCategory,
forEachEnvelopeItem,
serializeEnvelope,
Expand Down Expand Up @@ -57,6 +58,11 @@ export function createTransport(

// Creates client report for each item in an envelope
const recordEnvelopeLoss = (reason: EventDropReason): void => {
// Don't record outcomes for client reports - we don't want to create a feedback loop if client reports themselves fail to send
if (envelopeContainsItemType(filteredEnvelope, ['client_report'])) {
DEBUG_BUILD && debug.warn(`Dropping client report. Will not send outcomes (reason: ${reason}).`);
return;
}
forEachEnvelopeItem(filteredEnvelope, (item, type) => {
options.recordDroppedEvent(reason, envelopeItemTypeToDataCategory(type));
});
Expand Down
89 changes: 88 additions & 1 deletion packages/core/test/lib/transports/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { describe, expect, it, vi } from 'vitest';
import { createTransport } from '../../../src/transports/base';
import type { ClientReport } from '../../../src/types-hoist/clientreport';
import type { AttachmentItem, EventEnvelope, EventItem } from '../../../src/types-hoist/envelope';
import type { TransportMakeRequestResponse } from '../../../src/types-hoist/transport';
import { createClientReportEnvelope } from '../../../src/utils/clientreport';
import { createEnvelope, serializeEnvelope } from '../../../src/utils/envelope';
import type { PromiseBuffer } from '../../../src/utils/promisebuffer';
import { type PromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from '../../../src/utils/promisebuffer';
import { resolvedSyncPromise } from '../../../src/utils/syncpromise';

const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [
Expand Down Expand Up @@ -31,6 +33,25 @@ const ATTACHMENT_ENVELOPE = createEnvelope<EventEnvelope>(
],
);

const defaultDiscardedEvents: ClientReport['discarded_events'] = [
{
reason: 'before_send',
category: 'error',
quantity: 30,
},
{
reason: 'network_error',
category: 'transaction',
quantity: 23,
},
];

const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
defaultDiscardedEvents,
'https://public@dsn.ingest.sentry.io/1337',
123456,
);

const transportOptions = {
recordDroppedEvent: () => undefined, // noop
};
Expand Down Expand Up @@ -304,5 +325,71 @@ describe('createTransport', () => {
expect(recordDroppedEventCallback).not.toHaveBeenCalled();
});
});

describe('Client Reports', () => {
it('should not record outcomes when client reports fail to send', async () => {
expect.assertions(2);

const mockRecordDroppedEventCallback = vi.fn();

const transport = createTransport({ recordDroppedEvent: mockRecordDroppedEventCallback }, req => {
expect(req.body).toEqual(serializeEnvelope(CLIENT_REPORT_ENVELOPE));
return Promise.reject(new Error('Network error'));
});

try {
await transport.send(CLIENT_REPORT_ENVELOPE);
} catch (e) {
// Expected to throw
}

// recordDroppedEvent should NOT be called when a client report fails
expect(mockRecordDroppedEventCallback).not.toHaveBeenCalled();
});

it('should not record outcomes when client reports fail due to buffer overflow', async () => {
expect.assertions(2);

const mockRecordDroppedEventCallback = vi.fn();
const mockBuffer: PromiseBuffer<TransportMakeRequestResponse> = {
$: [],
add: vi.fn(() => Promise.reject(SENTRY_BUFFER_FULL_ERROR)),
drain: vi.fn(),
};

const transport = createTransport(
{ recordDroppedEvent: mockRecordDroppedEventCallback },
_ => resolvedSyncPromise({}),
mockBuffer,
);

const result = await transport.send(CLIENT_REPORT_ENVELOPE);

// Should resolve without throwing
expect(result).toEqual({});
// recordDroppedEvent should NOT be called when a client report fails
expect(mockRecordDroppedEventCallback).not.toHaveBeenCalled();
});

it('should record outcomes when regular events fail to send', async () => {
expect.assertions(2);

const mockRecordDroppedEventCallback = vi.fn();

const transport = createTransport({ recordDroppedEvent: mockRecordDroppedEventCallback }, req => {
expect(req.body).toEqual(serializeEnvelope(ERROR_ENVELOPE));
return Promise.reject(new Error('Network error'));
});

try {
await transport.send(ERROR_ENVELOPE);
} catch (e) {
// Expected to throw
}

// recordDroppedEvent SHOULD be called for regular events
expect(mockRecordDroppedEventCallback).toHaveBeenCalledWith('network_error', 'error');
});
});
});
});
Loading