Reporting configuration and adapter contracts#
The reporting plugin contract supersedes the separate extension roles described below. This page retains the supported compatibility API: legacy collection runs through the same plugin operation coordinator. New consumers should register plugins with owned settings, capabilities and optional tab contributions.
Status: accepted configuration foundation from #81, with capture behavior revisions implemented by #126. The user selected stable registration IDs with later-registration precedence (Q16, superseding Q14's duplicate-error preference), and isolated runtime failures with retained evidence/drafts (Q15). This document describes the first implementation of those contracts; the original contract was accepted and merged. The accepted C interface is preserved, with real collector wiring in #82–#84.
Compose selected adapters#
@snagbadger/configuration owns composition, selection and resolution. It imports no integrations, browser globals, React or CSS. Plain objects work directly; helpers are optional independent named exports. Presets are ordinary configuration objects/functions, with no special permission mode. IDs identify registrations within an adapter kind, not adapter implementations: one implementation can be configured in several slots. Capture registrations additionally have an optional category ID for mutually exclusive alternatives.
import {
composeConfiguration,
pipeConfiguration,
removeAdapter,
} from '@snagbadger/configuration';
import {
createReportingRuntime,
type ReportingAdapters,
} from '@snagbadger/reporting/session';
const configuration = composeConfiguration<ReportingAdapters>(qaPreset, {
ui: { zIndex: 40 },
adapters: {
harvesters: [
{
id: 'http',
label: 'Network requests',
load: () => withCompanyFiltering(httpHarvester()),
settings: [
{
id: 'bodies',
label: 'Request bodies',
type: 'boolean',
default: false,
editable: false,
},
],
},
],
},
});
const narrowed = pipeConfiguration(configuration, (input) =>
removeAdapter(input, 'harvesters', 'websocket'),
);
const reporter = createReportingRuntime(narrowed);
qaPreset, httpHarvester and withCompanyFiltering above illustrate developer composition; production built-ins are delivered in #82. The executable study in examples/playground/configuration-fixtures.ts uses explicitly named fixture implementations and an actual wrapper that changes collected labels.
- Categories stay consumer-typed: reporting supplies harvesters, captures, providers and a caller-selected launcher type. There is no central list of every integration.
- Later scalar settings win;
uifields combine. Registrations combine by ID within their category. The last complete registration wins, including its loader, label, enablement and settings tree. Earlier position is retained for deterministic UI order; new IDs append. IDs are case-sensitive and nonempty. Different categories may use the same ID. removeAdapterremoves an ID.replaceAdaptersreplaces a whole category; pass[]to clear it. Merely composing[]adds nothing. Neither mutates its input.selectConfigurationevaluates host-owned predicates in order, stopping at the first match. Predicates may be asynchronous. No match disables reporting unless a fallback is supplied. Predicate errors reject initialization; they never activate adapters.resolveConfigurationcreates an immutable initialization snapshot, checks IDs/labels/defaults and copies nested settings. It never invokesload. Changing configuration requires disposing and creating another runtime. Functions/closures remain host-owned references: they must not read mutable configuration to bypass the resolved policy.enabled: falsedisables reporting. A disabled registration cannot be selected. Unknown selections/settings fail before loaders run. Factories/imports/loaders must be inert; resource acquisition belongs instart.
Settings structure and permissions#
Each registration can carry a recursive Setting tree. Boolean, bounded numeric and enumerated-choice nodes have IDs, labels, defaults, optional children and an explicit editable flag. This is portable descriptive data; UI rendering remains optional and replaceable. It can drive the accepted master/child controls without embedding React in a harvester. Short choices can use segmented controls and larger lists dropdowns.
Node IDs are path segments without dots. Nested values use dotted paths, scoped to their registration. For example settings['harvesters/http']['payload.limit']. Duplicate paths, invalid defaults, unknown paths, locked changes and values outside the developer's range/options are errors. Editable defaults describe developer-authorized alternatives; setting a default to false does not itself prohibit an editable true choice. For prohibition, lock it false or disable the adapter.
Child values remain preferences: a collector must treat an off parent as disabling its entire branch, and the UI retains child choices while hiding/disablement is active. The adapter's own options/factory remain the final implementation of filtering, redaction, retention and byte limits. These guarantees must be verified for every real built-in in #82/#83; settings metadata alone cannot constrain malicious developer-authored code.
Consumer-owned adapters#
Capture owns CaptureAdapter, ActiveCapture, CaptureOutput and AdapterContext in @snagbadger/capture. Reporting owns Harvester and runtime coordination in @snagbadger/reporting/session. Embedding owns LauncherAdapter; callers pass its type to ReportingAdapters<LauncherAdapter>. Reporting's existing ReportProvider, BugReport and portable bundle format are reused, rather than defining another delivery/report representation.
const customHarvester: Harvester = {
start(context) {
// Acquire only now; obey cancellation and developer bounds.
const collector = subscribeToAllowedAppEvents(context);
return {
collect: () => collector.snapshot(),
dispose: () => collector.unsubscribe(),
};
},
};
const wrapped: Harvester = {
async start(context) {
const active = await customHarvester.start(context);
return {
collect: async () => sanitize(await active.collect()),
dispose: () => active.dispose(),
};
},
};
Wrappers return the same interface as the wrapped adapter. No separate “adapter for adapters” registry or mandatory inheritance is needed. A capture additionally declares snapshot/recording mode. A launcher mounts using open/stop callbacks, updates idle/recording/open state and provides disposal; the browser embedding implementation owns its host DOM/style isolation. A provider implements the existing deliver({ report, bundle }) method. Registration loaders can defer importing these implementations; the core does not import built-ins.
Capture output contains canonical evidence/interactions and attachment bytes keyed by evidence ID. Output is validated against existing report decoders. Attachment bytes are copied before returning; unknown attachment keys and conflicting/invalid evidence are failures. Adapters own unique evidence/interaction IDs, for example by incorporating their registration/session identity. Successful results merge in selection order, independently of asynchronous startup order. A conflicting adapter's complete output is rejected rather than overwriting prior evidence.
Activation, cancellation and cleanup#
const session = reporter.start({
harvesters: ['http', 'app'],
captures: ['screenshot', 'steps'],
});
await session.ready;
// The optional reporting UI now lets the reporter reproduce the issue.
const result = await session.finish();
// result.evidence, result.interactions, result.attachments, result.failures
await reporter.dispose();
Runtime construction is inactive. start is an explicit interaction boundary and exposes the session immediately, so cancel is available during asynchronous loading/activation. Only selected enabled adapters load and start. The runtime permits one active session and rejects overlapping starts. Repeated finish/cancel calls settle the same result; cancellation notifies adapters and abandons waiting on pending activation/collection. No cancellation exceptions become artificial adapter failures. Active resources are disposed once, on finish/cancel/dispose, and another session receives fresh instances.
AdapterContext.cancellation is a portable subscription interface. Adapters bridge it to AbortController or their library's teardown without requiring DOM globals in functions-only imports. A noncooperative startup that resolves after cancellation has its resource disposed when it arrives. Cancellation cannot physically stop arbitrary custom work; adapters must cooperate. Such late cleanup may occur after the cancellation result and its cleanup errors are not retroactively included in that settled result. Real integrations require cancellation/resource conformance tests.
Cancellation abandons activation and collection waits, but waits for cleanup of resources already acquired. dispose() must settle after releasing its resources. If custom cleanup never settles, finish/cancel/runtime disposal remain pending and the runtime rejects another session until cleanup completes. This deliberately preserves resource exclusivity; there is no timeout that silently abandons live resources. Cleanup errors that reject are isolated. Prompt cancellation therefore depends on cooperative, terminating cleanup, and every real adapter needs a cleanup conformance test.
start({ preRecording: true, harvesters: [...] }) is explicitly opted-in diagnostics and rejects capture selections. Developer configuration must first allow it with preRecording: true (default permission is false); this permission itself never starts collection. Context marks the phase and supplies the registration ID. Finish/cancel/dispose stop it; a subsequent session defaults to ordinary recording. The reporting UI owns the default-off/reset checkbox. Reload persistence/activation and carrying pre-recorded diagnostics into the main report remain #82/#84 work; this runtime never automatically reloads, installs listeners or stores an opt-in. HTTP/WebSocket/browser/app are separate registrations; existing sockets cannot yield unobserved earlier messages.
Failure and submission behavior#
Configuration/selection errors prevent activation. Runtime start/collect/dispose errors produce { kind, id, phase, message } failures. Diagnostic-harvester errors retain successful output. A selected capture error instead stops the complete attempt and discards its evidence, interactions and attachments; the UI shows the error while the caller preserves its prior draft and attachments. This supersedes the original mixed-success capture behavior. Session completed resolves on normal finish, cancellation, capture failure or resource-originated completion. Capture adapters can invoke context finish for native Stop sharing and fail for asynchronous capture failure. UI and providers must clearly represent unavailable evidence. Each adapter owns its cleanup and host-behavior preservation, including restoring wrapped functions only when it still owns the wrapper.
Providers load only on explicit runtime.provider(id). Delivery uses the existing submission/bundle helpers and their explicit sanitization policy. The runtime never mutates or clears a report draft; the caller retains it after failed delivery and retries deliberately. It does not claim cross-provider retry/idempotency, cancellation of external delivery, authentication, shared-device PIN security or ticket retrieval. These remain with #11. runtime.dispose() disables further session/provider access; resources returned by a provider loader must remain inert until delivery.
Review and remaining work#
Run pnpm dev:examples, then open playground/branding/?page=configuration. The contract study shows the original HTTP registration replaced by a wrapped custom one, untouched app registration, isolated failure, custom capture and custom local provider using a real portable bundle. All collection/delivery adapters there are fixtures: it does not capture host traffic/screenshots or contact providers.
Direct tests cover composition, selector failure/no-match, immutable settings/defaults/permissions, inactivity, built-in/custom lifecycle parity, pre-recording reset, cancellation during startup, late cleanup, runtime reuse/disposal, and isolated failures. Browser verification exercises the study twice and preserves the accepted reporter route.
This slice establishes composition and portable lifecycle contracts, not the production in-page SDK. #82 provides four actual harvesters and retention/privacy/host-behavior verification; #83 provides real screenshots/steps/visual replay and capture-specific permission/navigation decisions; #84 wires the accepted drawer, local export/import and report draft. UI descriptors, custom renderers, provider credentials and launcher installation are consumed there through their respective seams. Human review settles #81 before dependent delivery; no merge/release/deployment is implied.
Capture alternatives use distinct registration IDs and the same category. captureCategories exposes enabled alternatives and the first configured default; selecting two captures in the same category fails before loading. The accepted recording category offers either DOM replay or screen video. See the capture experiment for shared display sources, screenshot timing and current video limitations.