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/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// Browser platform: fetch transport, IndexedDB storage, window error handlers, stack-trace parser,
// and the launch() composition root. Tier 2. See docs/design/sdk-design.md 搂5.

// Lifecycle events: the event-name constants + listener type for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
export { COMPONENT_ATTRIBUTE, componentNameFromElement } from './component-name';
export {
createUnhandledRejectionProvider,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ export async function launchCore(
setUserIdentifier: (email) => client.setUserIdentifier(email),
clearUserIdentifier: () => client.clearUserIdentifier(),
sdkVersion: SDK_VERSION,
emitLifecycleEvent: (eventType, data) => client.emitLifecycleEvent(eventType, data),
ui: {
summaryRequired: reportUi.get(BugseeOption.ReportingUiSummaryRequired, false),
descriptionRequired: reportUi.get(BugseeOption.ReportingUiDescriptionRequired, false),
Expand Down
23 changes: 23 additions & 0 deletions packages/browser/src/report-dialog.launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ describe('client.showReportDialog (launched browser client)', () => {
expect(request.type).toBe('bug');
});

it('drives the lifecycle listener end to end: dialog shown, report assembled, uploaded, dialog gone', async () => {
const { client } = await start();
const seen: string[] = [];
client.setLifecycleEventsListener((type) =>
seen.push(type.replace('com.bugsee.lifecycle.', '')),
);
const pending = client.showReportDialog();
fill('summary', 'x');
dialog()?.querySelector<HTMLButtonElement>('[data-action="send"]')?.click();
await pending;

expect(seen).toEqual([
'BeforeReportShown',
'BeforeReportAssembled',
'AfterReportAssembled',
'BeforeReportUploaded',
'AfterReportUploaded',
'AfterReportShown',
]);
await client.stop();
expect(seen.slice(-2)).toEqual(['Stopping', 'Stopped']);
});

it('honours the reporting-UI launch options', async () => {
const { client, http } = await start({
reportSummaryRequired: true,
Expand Down
50 changes: 49 additions & 1 deletion packages/browser/src/report-dialog.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// @vitest-environment jsdom
import type { PreparedUpload, UploadOptions, UploadResult } from '@bugsee/core';
import {
LifecycleEvents,
type PreparedUpload,
type UploadOptions,
type UploadResult,
} from '@bugsee/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
createReportDialogLauncher,
Expand Down Expand Up @@ -1079,3 +1084,46 @@ describe('strings overrides', () => {
expect(field('summary').placeholder).toBe('Short summary');
});
});

describe('lifecycle events', () => {
it('announces BeforeReportShown when the dialog opens and AfterReportShown once it is gone', async () => {
const emit = vi.fn();
const { show } = harness({ emitLifecycleEvent: emit });
const pending = show();

expect(emit.mock.calls).toEqual([[LifecycleEvents.BeforeReportShown]]);
expect(host()).not.toBeNull(); // Before fired as the dialog went up, not after it was dismissed

send();
await pending;
expect(emit.mock.calls).toEqual([
[LifecycleEvents.BeforeReportShown],
[LifecycleEvents.AfterReportShown, { sent: true }],
]);
});

it('announces AfterReportShown with sent:false when the user discards', async () => {
const emit = vi.fn();
const { show } = harness({ emitLifecycleEvent: emit });
const pending = show();
click('[data-action="cancel"]');
await pending;

expect(emit).toHaveBeenLastCalledWith(LifecycleEvents.AfterReportShown, { sent: false });
expect(emit).toHaveBeenCalledTimes(2);
});

it('announces nothing when no dialog is shown (cannot prepare, or SDK not running)', async () => {
const emit = vi.fn();
await harness({ emitLifecycleEvent: emit, prepare: () => null }).show();
await harness({ emitLifecycleEvent: emit, isLaunched: () => false }).show();
expect(emit).not.toHaveBeenCalled();
});

it('works without a lifecycle hook', async () => {
const { show } = harness();
const pending = show();
send();
await expect(pending).resolves.toEqual({ ok: true });
});
});
18 changes: 17 additions & 1 deletion packages/browser/src/report-dialog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { PreparedUpload, UploadResult } from '@bugsee/core';
import {
LifecycleEvents,
type LifecycleEventType,
type PreparedUpload,
type UploadResult,
} from '@bugsee/core';
import { DEFAULT_REPORT_DIALOG_STRINGS, type ReportDialogStrings } from './report-dialog-strings';
import { REPORT_DIALOG_CLASS_PREFIX, REPORT_DIALOG_CSS } from './report-dialog-styles';

Expand Down Expand Up @@ -90,6 +95,11 @@ export interface ReportDialogDeps {
/** Shown in the footer as `v<sdkVersion>`. */
sdkVersion: string;
ui: ReportDialogUiOptions;
/**
* Lifecycle hook (`client.emitLifecycleEvent`): told BeforeReportShown just before the dialog is put on
* the page and AfterReportShown once it is gone, however it closed. Never expected to throw.
*/
emitLifecycleEvent?: (eventType: LifecycleEventType, data?: unknown) => void;
}

const SVG_NS = 'http://www.w3.org/2000/svg';
Expand Down Expand Up @@ -194,6 +204,7 @@ function showDialog(
const previousOverflow = doc.documentElement.style.overflow;
let busy = false;
let closed = false;
let announced = false;

const fieldValue = (name: FieldName): string => refs.fields[name]?.value.trim() ?? '';
const severityValue = (): PickerSeverity =>
Expand All @@ -215,6 +226,9 @@ function showDialog(
return;
}
closed = true;
if (announced) {
deps.emitLifecycleEvent?.(LifecycleEvents.AfterReportShown, { sent: result !== null });
}
if (result === null) {
prepared.discard();
}
Expand Down Expand Up @@ -405,6 +419,8 @@ function showDialog(
}

try {
announced = true;
deps.emitLifecycleEvent?.(LifecycleEvents.BeforeReportShown);
doc.documentElement.append(refs.host);
doc.documentElement.style.overflow = 'hidden';
const dialog = refs.dialog as HTMLDialogElement;
Expand Down
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,11 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
export type { BugseeSpanProcessor } from '@bugsee/opentelemetry';
export { type BugseeBunLaunchOptions, launch } from './bun';
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
export type { Bugsee } from '@bugsee/deno';
export type { BugseeSpanProcessor } from '@bugsee/opentelemetry';
export { type BugseeDenoLaunchOptions, launch } from './deno';
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.edge-light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,11 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
export * from '@bugsee/vercel-edge';
export { launchEdge as launch } from '@bugsee/vercel-edge';
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
// F-6: the `HttpTransport` primitive (+ its request/response option shapes), so a custom transport (a
// proxy, a queueing shim, a test double) passed to the `transport` launch option types without an
// unsafe cast.
Expand Down
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
// The SpanProcessor type users receive via `onOtelSpanProcessor` to register on their TracerProvider.
export type { BugseeSpanProcessor } from '@bugsee/opentelemetry';
// The performance extension's public surface. Re-exported for a reason beyond convenience:
Expand Down
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
export * from '@bugsee/webworker';
6 changes: 6 additions & 0 deletions packages/bugsee/src/index.workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export type {
LogExceptionOptions,
UploadOptions,
} from '@bugsee/core';
// Lifecycle events: the listener type + the event-name constants for `setLifecycleEventsListener`.
export {
LifecycleEvents,
type LifecycleEventsListener,
type LifecycleEventType,
} from '@bugsee/core';
28 changes: 28 additions & 0 deletions packages/bugsee/src/lifecycle-exports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LifecycleEvents } from '@bugsee/core';
import { describe, expect, it } from 'vitest';
import * as browserEntry from './index';
import * as bunEntry from './index.bun';
import * as denoEntry from './index.deno';
import * as edgeEntry from './index.edge-light';
import * as nodeEntry from './index.node';
import * as workerEntry from './index.worker';
import * as workerdEntry from './index.workerd';

// Every umbrella entry re-exports the lifecycle constants (the same object as core's), so
// `setLifecycleEventsListener` is usable on every runtime without reaching into @bugsee/core.
describe('LifecycleEvents is re-exported from every umbrella entry', () => {
const entries = {
browser: browserEntry,
node: nodeEntry,
bun: bunEntry,
deno: denoEntry,
'edge-light': edgeEntry,
worker: workerEntry,
workerd: workerdEntry,
};
for (const [name, entry] of Object.entries(entries)) {
it(name, () => {
expect(entry.LifecycleEvents).toBe(LifecycleEvents);
});
}
});
12 changes: 12 additions & 0 deletions packages/bugsee/src/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
AttributeValue,
Breadcrumb,
BreadcrumbInput,
LifecycleEventsListener,
LifecycleEventType,
LogExceptionOptions,
ReportDialogOptions,
ReportDialogStrings,
Expand Down Expand Up @@ -33,4 +35,14 @@ describe('@bugsee/bugsee public API surface', () => {
const strings = {} as ReportDialogStrings;
expect([upload, dialog, strings]).toHaveLength(3);
});

it('re-exports the lifecycle event constants and listener types', () => {
const listener: LifecycleEventsListener = () => {};
const type: LifecycleEventType = publicApi.LifecycleEvents.Launched;
expect(type).toBe('com.bugsee.lifecycle.Launched');
expect(publicApi.LifecycleEvents.AfterReportUploaded).toBe(
'com.bugsee.lifecycle.AfterReportUploaded',
);
expect(typeof listener).toBe('function');
});
});
Loading
Loading