Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 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,10 @@ 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'])) {
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