Skip to content
Open
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
18 changes: 8 additions & 10 deletions test/e2e/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,17 @@ export function electronTestRunner(
});
Comment thread
timfish marked this conversation as resolved.

expect(unorderedEvents).toEqual(expect.arrayContaining(expectedEvents));
}
} catch (e) {
reject?.(e);
}

if (expectations.length === 0) {
if (options.waitAfterExpectedEvents) {
delay(options.waitAfterExpectedEvents).then(() => {
if (options.waitAfterExpectedEvents) {
delay(options.waitAfterExpectedEvents).then(() => {
resolve?.();
});
} else {
resolve?.();
});
} else {
resolve?.();
}
}
} catch (e) {
reject?.(e);
}
}

Expand Down
1 change: 1 addition & 0 deletions test/e2e/test-apps/other/browser-profiling-manual/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

electronTestRunner(__dirname, async (ctx) => {
await ctx
.ignoreExpectationOrder()
// Expect the transaction (without attached profile since we're using UI profiling)
.expect({
Comment thread
sentry[bot] marked this conversation as resolved.
envelope: transactionEnvelope({
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,10 @@ export function transactionEnvelope(event: TransactionEvent, ...otherEnvelopeIte

export function sessionEnvelope(session: SerializedSession): Envelope {
return [
{
expect.objectContaining({
sent_at: ISO_DATE_MATCHER,
sdk: { name: 'sentry.javascript.electron', version: SDK_VERSION },
},
}),
[
[
{ type: 'session' },
Expand Down
278 changes: 144 additions & 134 deletions test/unit/minidump-loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import '../../scripts/electron-shim.mjs';
import { uuid4 } from '@sentry/core';
import { closeSync, existsSync, openSync, utimesSync, writeFileSync, writeSync } from 'fs';
import { existsSync, utimesSync, writeFileSync, promises as fsPromises } from 'fs';
import { join } from 'path';
import * as tmp from 'tmp';
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';

const { createMinidumpLoader } = await import('../../src/main/integrations/sentry-minidump/minidump-loader');

function dumpFileName(): string {
return `${uuid4()}.dmp`;
}

function setMtime(path: string, date: Date): void {
utimesSync(path, date, date);
}

const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'X'.repeat(12_000)}`);
const LOOKS_NOTHING_LIKE_A_MINIDUMP = Buffer.from('X'.repeat(12_000));
const MINIDUMP_HEADER_BUT_TOO_SMALL = Buffer.from('MDMPdflahfalfhalkfnaklsfnalfkn');

describe('createMinidumpLoader', () => {
let tempDir: tmp.DirResult;

beforeAll(() => {
tempDir = tmp.dirSync({ unsafeCleanup: true });
});
Expand All @@ -27,136 +32,141 @@ describe('createMinidumpLoader', () => {
}
});

test('creates attachment from minidump', () =>
new Promise<void>((done) => {
const name = dumpFileName();
const dumpPath = join(tempDir.name, name);
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

void loader(false, async (_, attachment) => {
expect(attachment).to.eql({
data: VALID_LOOKING_MINIDUMP,
filename: name,
attachmentType: 'event.minidump',
});

setTimeout(() => {
expect(existsSync(dumpPath)).to.be.false;
done();
}, 1_000);
});
}));

test("doesn't send invalid minidumps", () =>
new Promise<void>((done) => {
const missingHeaderDump = join(tempDir.name, dumpFileName());
writeFileSync(missingHeaderDump, LOOKS_NOTHING_LIKE_A_MINIDUMP);
const tooSmallDump = join(tempDir.name, dumpFileName());
writeFileSync(tooSmallDump, MINIDUMP_HEADER_BUT_TOO_SMALL);

const loader = createMinidumpLoader(() => Promise.resolve([missingHeaderDump, tooSmallDump]));

let passedAttachment = false;
void loader(false, async () => {
passedAttachment = true;
});

setTimeout(() => {
expect(passedAttachment).to.be.false;
expect(existsSync(missingHeaderDump)).to.be.false;
expect(existsSync(tooSmallDump)).to.be.false;
done();
}, 2_000);
}));

test("doesn't send minidumps that are over 30 days old", () =>
new Promise<void>((done) => {
const dumpPath = join(tempDir.name, dumpFileName());
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);
const thirtyOneDaysAgo = new Date(new Date().getTime() - 31 * 24 * 3_600 * 1_000);
utimesSync(dumpPath, thirtyOneDaysAgo, thirtyOneDaysAgo);

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let passedAttachment = false;
void loader(false, async () => {
passedAttachment = true;
});

setTimeout(() => {
expect(passedAttachment).to.be.false;
expect(existsSync(dumpPath)).to.be.false;
done();
}, 2_000);
}));

test('deletes minidumps when sdk is disabled', () =>
new Promise<void>((done) => {
const dumpPath = join(tempDir.name, dumpFileName());
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let passedAttachment = false;
void loader(true, async () => {
passedAttachment = true;
});

setTimeout(() => {
expect(passedAttachment).to.be.false;
expect(existsSync(dumpPath)).to.be.false;
done();
}, 2_000);
}));

test(
'waits for minidump to stop being modified',
{ timeout: 10_000, repeats: 2 },
() =>
new Promise<void>((done) => {
const dumpPath = join(tempDir.name, dumpFileName());
const file = openSync(dumpPath, 'w');
writeSync(file, VALID_LOOKING_MINIDUMP);

let count = 0;
// Write the file every 500ms
const timer = setInterval(() => {
count += 500;
writeSync(file, 'X');
}, 500);

setTimeout(() => {
clearInterval(timer);
closeSync(file);
}, 4_200);

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

void loader(false, async (_) => {
expect(count).to.be.greaterThanOrEqual(3_000);
done();
});
}),
);

test('sending continues after loading failures', () =>
new Promise<void>((done) => {
const missingPath = join(tempDir.name, dumpFileName());
const name = dumpFileName();
const dumpPath = join(tempDir.name, name);
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);

const loader = createMinidumpLoader(() => Promise.resolve([missingPath, dumpPath]));

void loader(false, async (_, attachment) => {
expect(attachment.filename).to.eql(name);

setTimeout(() => {
expect(existsSync(dumpPath)).to.be.false;
done();
}, 1_000);
});
}));
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});

test('creates attachment from minidump', async () => {
const name = dumpFileName();
const dumpPath = join(tempDir.name, name);
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);
setMtime(dumpPath, new Date(Date.now() - 2_000));

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let attachment: unknown;
await loader(false, async (_, att) => {
attachment = att;
});

expect(attachment).to.eql({
data: VALID_LOOKING_MINIDUMP,
filename: name,
attachmentType: 'event.minidump',
});
expect(existsSync(dumpPath)).to.be.false;
});

test("doesn't send invalid minidumps", async () => {
const missingHeaderDump = join(tempDir.name, dumpFileName());
writeFileSync(missingHeaderDump, LOOKS_NOTHING_LIKE_A_MINIDUMP);
setMtime(missingHeaderDump, new Date(Date.now() - 2_000));
const tooSmallDump = join(tempDir.name, dumpFileName());
writeFileSync(tooSmallDump, MINIDUMP_HEADER_BUT_TOO_SMALL);
setMtime(tooSmallDump, new Date(Date.now() - 2_000));

const loader = createMinidumpLoader(() => Promise.resolve([missingHeaderDump, tooSmallDump]));

let passedAttachment = false;
await loader(false, async () => {
passedAttachment = true;
});

expect(passedAttachment).to.be.false;
expect(existsSync(missingHeaderDump)).to.be.false;
expect(existsSync(tooSmallDump)).to.be.false;
});

test("doesn't send minidumps that are over 30 days old", async () => {
const dumpPath = join(tempDir.name, dumpFileName());
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);
setMtime(dumpPath, new Date(Date.now() - 31 * 24 * 3_600 * 1_000));

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let passedAttachment = false;
await loader(false, async () => {
passedAttachment = true;
});

expect(passedAttachment).to.be.false;
expect(existsSync(dumpPath)).to.be.false;
});

test('deletes minidumps when sdk is disabled', async () => {
const dumpPath = join(tempDir.name, dumpFileName());
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);
setMtime(dumpPath, new Date(Date.now() - 2_000));

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let passedAttachment = false;
await loader(true, async () => {
passedAttachment = true;
});

expect(passedAttachment).to.be.false;
expect(existsSync(dumpPath)).to.be.false;
});

test('waits for minidump to stop being modified', async () => {
const dumpPath = join(tempDir.name, dumpFileName());
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);

// Track the mtime the loader will see - starts at "now" (recently modified)
let fakeMtime = Date.now();

// Mock fs.promises.stat so the retry loop resolves via microtasks rather than real
// I/O callbacks. Real I/O callbacks fire in the event-loop poll phase, which
// vi.advanceTimersByTimeAsync can't guarantee to drain between fake timer fires.
vi.spyOn(fsPromises, 'stat').mockImplementation(async () => {
return { mtimeMs: fakeMtime } as any;
});

let writeCount = 0;
const timer = setInterval(() => {
writeCount++;
fakeMtime = Date.now(); // keep mtime in sync with advancing fake clock
}, 500);
setTimeout(() => clearInterval(timer), 3_000);

const loader = createMinidumpLoader(() => Promise.resolve([dumpPath]));

let callbackCalled = false;
const loaderPromise = loader(false, async () => {
callbackCalled = true;
});

// Advance past writes stopping (3000ms) + NOT_MODIFIED_MS (1000ms) + one retry (500ms)
await vi.advanceTimersByTimeAsync(6_000);
// After the advance the mtime check passes, but fs.readFile/unlink are real I/O —
// await the loader promise before asserting so that I/O settles.
await loaderPromise;

expect(callbackCalled).toBe(true);
expect(writeCount).toBeGreaterThanOrEqual(5);
});

test('sending continues after loading failures', async () => {
const missingPath = join(tempDir.name, dumpFileName());
const name = dumpFileName();
const dumpPath = join(tempDir.name, name);
writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP);
setMtime(dumpPath, new Date(Date.now() - 2_000));

const loader = createMinidumpLoader(() => Promise.resolve([missingPath, dumpPath]));

let receivedName: string | undefined;
await loader(false, async (_, attachment) => {
receivedName = attachment.filename;
});

expect(receivedName).to.eql(name);
expect(existsSync(dumpPath)).to.be.false;
});
});
Loading