@feltdb/core 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/application-lifecycle.js +112 -0
- package/dist/cli/application.js +10 -0
- package/dist/cli/commands.js +407 -119
- package/dist/cli/development-session.js +54 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +146 -20
- package/dist/collection.d.ts +110 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +228 -3
- package/dist/create/cli.js +4 -2
- package/dist/create/create.js +42 -6
- package/dist/create/frameworks.js +18 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
- package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
- package/dist/create/workspace-initialization.js +4 -5
- package/dist/db.d.ts +63 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +145 -4
- package/dist/development-runtime-bridge.d.ts +60 -0
- package/dist/development-runtime-bridge.d.ts.map +1 -0
- package/dist/development-runtime-bridge.js +171 -0
- package/dist/embedded-transaction.d.ts +77 -0
- package/dist/embedded-transaction.d.ts.map +1 -0
- package/dist/embedded-transaction.js +102 -0
- package/dist/feltdb.d.ts +49 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +33 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +128 -4
- package/dist/freshness.d.ts +119 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +115 -0
- package/dist/http-db.d.ts +48 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +77 -1
- package/dist/index-backend.d.ts +8 -0
- package/dist/index-backend.d.ts.map +1 -1
- package/dist/index-backend.js +13 -0
- package/dist/index-core.d.ts +80 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +74 -0
- package/dist/index-manager.d.ts +11 -0
- package/dist/index-manager.d.ts.map +1 -1
- package/dist/index-manager.js +30 -13
- package/dist/index-types.d.ts +17 -0
- package/dist/index-types.d.ts.map +1 -1
- package/dist/index.browser.d.ts +14 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +13 -0
- package/dist/index.d.ts +8 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -54
- package/dist/indexeddb-db.d.ts +32 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +151 -1
- package/dist/memory-db.d.ts +26 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +86 -0
- package/dist/runtime-registry.d.ts +32 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +38 -0
- package/dist/studio/app.d.ts +2 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/StateExplorer.d.ts +2 -1
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
- package/dist/studio/index.js +234 -160
- package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
- package/dist/studio-app/assets/index-DospFFYE.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +98 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +86 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/browser.d.ts +22 -0
- package/dist/workspace/browser.d.ts.map +1 -0
- package/dist/workspace/browser.js +17 -0
- package/dist/workspace/development-node.d.ts +16 -0
- package/dist/workspace/development-node.d.ts.map +1 -1
- package/dist/workspace/development-node.js +85 -1
- package/dist/workspace/git-identity.d.ts +35 -0
- package/dist/workspace/git-identity.d.ts.map +1 -0
- package/dist/workspace/git-identity.js +81 -0
- package/dist/workspace/index.d.ts +12 -2
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +7 -1
- package/dist/workspace/investigation-analysis.d.ts +47 -0
- package/dist/workspace/investigation-analysis.d.ts.map +1 -0
- package/dist/workspace/investigation-analysis.js +181 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts +88 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
- package/dist/workspace/investigation-lifecycle-manager.js +259 -0
- package/dist/workspace/investigation-supervisor.d.ts +100 -0
- package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
- package/dist/workspace/investigation-supervisor.js +238 -0
- package/dist/workspace/runtime-investigation.d.ts +5 -1
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +50 -21
- package/dist/workspace/runtime-observation.d.ts +82 -0
- package/dist/workspace/runtime-observation.d.ts.map +1 -0
- package/dist/workspace/runtime-observation.js +170 -0
- package/dist/workspace/runtime-observer.d.ts +57 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -0
- package/dist/workspace/runtime-observer.js +272 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -5
- package/dist/workspace/workspace-identity.d.ts +1 -1
- package/dist/workspace/workspace-identity.d.ts.map +1 -1
- package/dist/workspace/workspace-identity.js +4 -4
- package/dist/workspace/workspace-types.d.ts +208 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -1
- package/package.json +12 -4
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/index-BF45M757.js +0 -28
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Runtime Observation Contract
|
|
3
|
+
*
|
|
4
|
+
* The single observation model shared by every runtime that reports into a
|
|
5
|
+
* FeltDB Development Workspace. The browser runtime in `@feltdb/core` produces
|
|
6
|
+
* these records, the workspace persists them durably, investigations are
|
|
7
|
+
* created from them, and verification is evaluated against them.
|
|
8
|
+
*
|
|
9
|
+
* This module is browser-safe: it must not import Node built-ins.
|
|
10
|
+
*/
|
|
11
|
+
/** Workspace collection holding durable runtime observations. */
|
|
12
|
+
export const RUNTIME_OBSERVATION_COLLECTION = 'runtime_observation';
|
|
13
|
+
/**
|
|
14
|
+
* Query parameter names whose values are replaced before an observation is
|
|
15
|
+
* persisted. Matching is case-insensitive and substring-based so that
|
|
16
|
+
* `access_token`, `apiKey`, and `X-Session` are all covered.
|
|
17
|
+
*/
|
|
18
|
+
export const REDACTED_PARAMETER_PATTERNS = [
|
|
19
|
+
'token', 'key', 'secret', 'password', 'passwd', 'auth', 'session',
|
|
20
|
+
'credential', 'signature', 'sig', 'code', 'assertion', 'bearer',
|
|
21
|
+
];
|
|
22
|
+
/** Placeholder substituted for a redacted value. */
|
|
23
|
+
export const REDACTION_PLACEHOLDER = '[redacted]';
|
|
24
|
+
function isRedactedParameter(name) {
|
|
25
|
+
const lowered = name.toLowerCase();
|
|
26
|
+
return REDACTED_PARAMETER_PATTERNS.some(pattern => lowered.includes(pattern));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Redact sensitive values from a URL while preserving the parts verification
|
|
30
|
+
* depends on: origin, pathname, and the set of query parameter names.
|
|
31
|
+
*
|
|
32
|
+
* FeltDB never captures request or response bodies, and never captures
|
|
33
|
+
* `Authorization` or `Cookie` headers, so URL redaction is the only place
|
|
34
|
+
* where credential material can reach a persisted observation.
|
|
35
|
+
*/
|
|
36
|
+
export function redactUrl(url) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(url, typeof location !== 'undefined' ? location.href : 'http://localhost');
|
|
39
|
+
if (parsed.username || parsed.password) {
|
|
40
|
+
parsed.username = '';
|
|
41
|
+
parsed.password = '';
|
|
42
|
+
}
|
|
43
|
+
for (const name of [...parsed.searchParams.keys()]) {
|
|
44
|
+
if (isRedactedParameter(name))
|
|
45
|
+
parsed.searchParams.set(name, REDACTION_PLACEHOLDER);
|
|
46
|
+
}
|
|
47
|
+
if (parsed.hash)
|
|
48
|
+
parsed.hash = '';
|
|
49
|
+
return parsed.toString();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A URL we cannot parse is a URL we cannot safely redact.
|
|
53
|
+
return REDACTION_PLACEHOLDER;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Redact a free-text runtime message that may embed a URL or credential. */
|
|
57
|
+
export function redactMessage(message) {
|
|
58
|
+
return message.replace(/https?:\/\/[^\s"'`)]+/g, match => redactUrl(match));
|
|
59
|
+
}
|
|
60
|
+
/** Browser family and engine derived from a user agent string. */
|
|
61
|
+
export function describeBrowser(userAgent) {
|
|
62
|
+
const ua = userAgent || '';
|
|
63
|
+
// Order matters: Brave/Edge/Opera all also claim "Chrome".
|
|
64
|
+
if (/\bBrave\//i.test(ua))
|
|
65
|
+
return { browser: 'Brave', engine: 'Blink' };
|
|
66
|
+
if (/\bEdg\//.test(ua))
|
|
67
|
+
return { browser: 'Edge', engine: 'Blink' };
|
|
68
|
+
if (/\bOPR\//.test(ua))
|
|
69
|
+
return { browser: 'Opera', engine: 'Blink' };
|
|
70
|
+
if (/\bFirefox\//.test(ua))
|
|
71
|
+
return { browser: 'Firefox', engine: 'Gecko' };
|
|
72
|
+
// `HeadlessChrome/` has no word boundary before "Chrome", so match the token
|
|
73
|
+
// directly. Headless Chrome is still Chrome, and still Blink.
|
|
74
|
+
if (/(?:Headless)?Chrome\//.test(ua))
|
|
75
|
+
return { browser: 'Chrome', engine: 'Blink' };
|
|
76
|
+
// Safari identifies itself last because Chromium also carries "Safari".
|
|
77
|
+
if (/\bSafari\//.test(ua) && /\bVersion\//.test(ua))
|
|
78
|
+
return { browser: 'Safari', engine: 'WebKit' };
|
|
79
|
+
return { browser: 'Unknown', engine: 'Unknown' };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether an observation represents an actionable runtime defect.
|
|
83
|
+
*
|
|
84
|
+
* FeltDB does not open an investigation for every request. Only these
|
|
85
|
+
* qualify:
|
|
86
|
+
* - a server-error response (HTTP >= 500)
|
|
87
|
+
* - a request that produced no response at all (network failure)
|
|
88
|
+
* - a client-error response correlated with a runtime or console error
|
|
89
|
+
*
|
|
90
|
+
* Successful requests, redirects, and bare 4xx responses (which are usually
|
|
91
|
+
* intentional application behavior) do not open investigations.
|
|
92
|
+
*/
|
|
93
|
+
export function isActionableRuntimeDefect(observation) {
|
|
94
|
+
if (observation.networkFailure)
|
|
95
|
+
return true;
|
|
96
|
+
if (observation.status >= 500)
|
|
97
|
+
return true;
|
|
98
|
+
if (observation.status >= 400) {
|
|
99
|
+
return (observation.correlatedEvents || [])
|
|
100
|
+
.some(event => event.kind === 'runtime_error' || event.kind === 'unhandled_rejection');
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/** Human-readable reason an observation was treated as actionable. */
|
|
105
|
+
export function describeDefect(observation) {
|
|
106
|
+
if (observation.networkFailure)
|
|
107
|
+
return 'the request produced no HTTP response';
|
|
108
|
+
if (observation.status >= 500)
|
|
109
|
+
return `the endpoint responded with HTTP ${observation.status}`;
|
|
110
|
+
return `the endpoint responded with HTTP ${observation.status} alongside a runtime error`;
|
|
111
|
+
}
|
|
112
|
+
let observationCounter = 0;
|
|
113
|
+
/** Generate a workspace-unique observation identifier. */
|
|
114
|
+
export function createObservationId() {
|
|
115
|
+
observationCounter += 1;
|
|
116
|
+
return `obs_${Date.now()}_${observationCounter}_${Math.random().toString(36).slice(2, 9)}`;
|
|
117
|
+
}
|
|
118
|
+
/** Generate a correlation identifier for a single in-flight request. */
|
|
119
|
+
export function createCorrelationId() {
|
|
120
|
+
return `corr_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
121
|
+
}
|
|
122
|
+
const FORBIDDEN_CHARACTERISTIC = /(authorization|cookie|headers?|body|payload|data)/i;
|
|
123
|
+
/** Keep diagnostic metadata factual while excluding headers and body-shaped values. */
|
|
124
|
+
function redactCharacteristics(characteristics) {
|
|
125
|
+
if (!characteristics)
|
|
126
|
+
return undefined;
|
|
127
|
+
return Object.fromEntries(Object.entries(characteristics)
|
|
128
|
+
.filter(([name]) => !FORBIDDEN_CHARACTERISTIC.test(name))
|
|
129
|
+
.map(([name, value]) => [name, typeof value === 'string' ? redactMessage(value) : value]));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build a redacted runtime observation.
|
|
133
|
+
*
|
|
134
|
+
* All redaction is applied here so that no caller can persist an unredacted
|
|
135
|
+
* observation by accident.
|
|
136
|
+
*/
|
|
137
|
+
export function createRuntimeObservation(input) {
|
|
138
|
+
const { browser, engine } = describeBrowser(input.userAgent || '');
|
|
139
|
+
return {
|
|
140
|
+
observationId: createObservationId(),
|
|
141
|
+
workspaceId: input.workspaceId,
|
|
142
|
+
sessionId: input.sessionId,
|
|
143
|
+
runtimeInstanceId: input.runtimeInstanceId,
|
|
144
|
+
correlation: input.correlation?.source ? {
|
|
145
|
+
source: {
|
|
146
|
+
product: input.correlation.source.product,
|
|
147
|
+
clientId: input.correlation.source.clientId,
|
|
148
|
+
investigationId: input.correlation.source.investigationId,
|
|
149
|
+
},
|
|
150
|
+
} : undefined,
|
|
151
|
+
method: input.method.toUpperCase(),
|
|
152
|
+
url: redactUrl(input.url),
|
|
153
|
+
status: input.status,
|
|
154
|
+
timestamp: input.completedAt,
|
|
155
|
+
durationMs: Math.max(0, input.completedAt - input.startedAt),
|
|
156
|
+
runtime: 'browser',
|
|
157
|
+
browser,
|
|
158
|
+
engine,
|
|
159
|
+
page: input.page ? redactUrl(input.page) : undefined,
|
|
160
|
+
correlationId: input.correlationId,
|
|
161
|
+
correlatedEvents: (input.correlatedEvents || []).map(event => ({
|
|
162
|
+
...event,
|
|
163
|
+
message: redactMessage(event.message),
|
|
164
|
+
stack: event.stack ? redactMessage(event.stack) : undefined,
|
|
165
|
+
})),
|
|
166
|
+
networkFailure: input.networkFailure || undefined,
|
|
167
|
+
requestCharacteristics: redactCharacteristics(input.requestCharacteristics),
|
|
168
|
+
responseCharacteristics: redactCharacteristics(input.responseCharacteristics),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Browser Runtime Observation
|
|
3
|
+
*
|
|
4
|
+
* The runtime that ships inside `@feltdb/core` and runs in the developer's
|
|
5
|
+
* browser. It instruments the page's HTTP and error surfaces, correlates
|
|
6
|
+
* console/runtime errors to the request that was in flight, and publishes
|
|
7
|
+
* redacted observations into the FeltDB Development Workspace, where they are
|
|
8
|
+
* persisted durably.
|
|
9
|
+
*
|
|
10
|
+
* Browser → @feltdb/core → observation → FeltDB workspace → durable persistence
|
|
11
|
+
*
|
|
12
|
+
* This module is browser-safe: it must not import Node built-ins. It uses only
|
|
13
|
+
* `fetch`, `XMLHttpRequest`, `console`, and `window` event handlers, so the
|
|
14
|
+
* same contract is produced by Chromium, Gecko, and WebKit browsers even
|
|
15
|
+
* though their internal implementations differ.
|
|
16
|
+
*/
|
|
17
|
+
import type { RuntimeRequestObservation } from './workspace-types.js';
|
|
18
|
+
export interface RuntimeObservationOptions {
|
|
19
|
+
/** Pairing code (FELT-XXXXXX) printed by `feltdb dev`. */
|
|
20
|
+
pairingCode?: string;
|
|
21
|
+
/** Pairing discovery endpoint, when it differs from the authority. */
|
|
22
|
+
discoveryEndpoint?: string;
|
|
23
|
+
/** Authority endpoint, when the workspace is already known. */
|
|
24
|
+
endpoint?: string;
|
|
25
|
+
/** Workspace id, when the workspace is already known. */
|
|
26
|
+
workspaceId?: string;
|
|
27
|
+
/** Canonical `feltdb dev` session identity supplied by the session handshake. */
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
/** Canonical runtime initialization identity supplied by the runtime bridge. */
|
|
30
|
+
runtimeInstanceId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* How long a failing request is held so that console and runtime errors
|
|
33
|
+
* emitted immediately afterwards can be correlated to it.
|
|
34
|
+
*/
|
|
35
|
+
correlationWindowMs?: number;
|
|
36
|
+
/** Expose the handle as `globalThis.__feltdbRuntime`. Defaults to true. */
|
|
37
|
+
exposeGlobal?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface RuntimeObservationHandle {
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
runtimeInstanceId?: string;
|
|
43
|
+
/** Observations produced by this page, in order. */
|
|
44
|
+
observations(): RuntimeRequestObservation[];
|
|
45
|
+
/** Resolve once every pending observation has been persisted. */
|
|
46
|
+
flush(): Promise<void>;
|
|
47
|
+
/** Restore the original browser APIs and disconnect from the workspace. */
|
|
48
|
+
stop(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Start observing the current page and reporting into the workspace.
|
|
52
|
+
*
|
|
53
|
+
* Returns once the workspace connection is established, so a caller can await
|
|
54
|
+
* it before exercising the application.
|
|
55
|
+
*/
|
|
56
|
+
export declare function startRuntimeObservation(options: RuntimeObservationOptions): Promise<RuntimeObservationHandle>;
|
|
57
|
+
//# sourceMappingURL=runtime-observer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-observer.d.ts","sourceRoot":"","sources":["../../src/workspace/runtime-observer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAA0B,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAS9F,MAAM,WAAW,yBAAyB;IACxC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,YAAY,IAAI,yBAAyB,EAAE,CAAC;IAC5C,iEAAiE;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,2EAA2E;IAC3E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAID;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA8PnC"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Browser Runtime Observation
|
|
3
|
+
*
|
|
4
|
+
* The runtime that ships inside `@feltdb/core` and runs in the developer's
|
|
5
|
+
* browser. It instruments the page's HTTP and error surfaces, correlates
|
|
6
|
+
* console/runtime errors to the request that was in flight, and publishes
|
|
7
|
+
* redacted observations into the FeltDB Development Workspace, where they are
|
|
8
|
+
* persisted durably.
|
|
9
|
+
*
|
|
10
|
+
* Browser → @feltdb/core → observation → FeltDB workspace → durable persistence
|
|
11
|
+
*
|
|
12
|
+
* This module is browser-safe: it must not import Node built-ins. It uses only
|
|
13
|
+
* `fetch`, `XMLHttpRequest`, `console`, and `window` event handlers, so the
|
|
14
|
+
* same contract is produced by Chromium, Gecko, and WebKit browsers even
|
|
15
|
+
* though their internal implementations differ.
|
|
16
|
+
*/
|
|
17
|
+
import { RUNTIME_OBSERVATION_COLLECTION, createCorrelationId, createRuntimeObservation, redactMessage, } from './runtime-observation.js';
|
|
18
|
+
import { connectDevelopmentWorkspace } from './workspace-connection.js';
|
|
19
|
+
const RUNTIME_EVENT_BUFFER = 50;
|
|
20
|
+
/**
|
|
21
|
+
* Start observing the current page and reporting into the workspace.
|
|
22
|
+
*
|
|
23
|
+
* Returns once the workspace connection is established, so a caller can await
|
|
24
|
+
* it before exercising the application.
|
|
25
|
+
*/
|
|
26
|
+
export async function startRuntimeObservation(options) {
|
|
27
|
+
const scope = globalThis;
|
|
28
|
+
const correlationWindowMs = options.correlationWindowMs ?? 250;
|
|
29
|
+
const connection = await connectDevelopmentWorkspace({
|
|
30
|
+
pairingCode: options.pairingCode,
|
|
31
|
+
workspaceId: options.workspaceId,
|
|
32
|
+
endpoint: options.endpoint,
|
|
33
|
+
discoveryEndpoint: options.discoveryEndpoint,
|
|
34
|
+
clientType: 'browser',
|
|
35
|
+
});
|
|
36
|
+
// Requests FeltDB itself makes must never be observed, or reporting an
|
|
37
|
+
// observation would produce another observation.
|
|
38
|
+
const selfOrigins = [options.endpoint, options.discoveryEndpoint]
|
|
39
|
+
.filter((value) => Boolean(value))
|
|
40
|
+
.map(value => value.replace(/\/$/, ''));
|
|
41
|
+
const isSelfRequest = (url) => {
|
|
42
|
+
try {
|
|
43
|
+
const absolute = new URL(url, scope.location ? scope.location.href : 'http://localhost').toString();
|
|
44
|
+
return selfOrigins.some(origin => absolute.startsWith(origin));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const emitted = [];
|
|
51
|
+
const pending = new Set();
|
|
52
|
+
const recentEvents = [];
|
|
53
|
+
const recordEvent = (event) => {
|
|
54
|
+
recentEvents.push(event);
|
|
55
|
+
if (recentEvents.length > RUNTIME_EVENT_BUFFER)
|
|
56
|
+
recentEvents.shift();
|
|
57
|
+
};
|
|
58
|
+
const eventsNear = (startedAt, until) => recentEvents.filter(event => event.timestamp >= startedAt && event.timestamp <= until);
|
|
59
|
+
const publish = (observation) => {
|
|
60
|
+
const record = { ...observation, workspaceId: connection.workspaceId };
|
|
61
|
+
emitted.push(record);
|
|
62
|
+
// The observation id is the entity id so the record is idempotent and
|
|
63
|
+
// directly addressable after a restart.
|
|
64
|
+
const task = connection
|
|
65
|
+
.publish(RUNTIME_OBSERVATION_COLLECTION, record)
|
|
66
|
+
.catch(() => undefined)
|
|
67
|
+
.finally(() => pending.delete(task));
|
|
68
|
+
pending.add(task);
|
|
69
|
+
};
|
|
70
|
+
const report = (input) => {
|
|
71
|
+
const completedAt = Date.now();
|
|
72
|
+
const build = (correlatedEvents) => createRuntimeObservation({
|
|
73
|
+
method: input.method,
|
|
74
|
+
url: input.url,
|
|
75
|
+
status: input.status,
|
|
76
|
+
startedAt: input.startedAt,
|
|
77
|
+
completedAt,
|
|
78
|
+
workspaceId: connection.workspaceId,
|
|
79
|
+
sessionId: options.sessionId,
|
|
80
|
+
runtimeInstanceId: options.runtimeInstanceId,
|
|
81
|
+
page: scope.location ? String(scope.location.href) : undefined,
|
|
82
|
+
userAgent: scope.navigator ? String(scope.navigator.userAgent) : undefined,
|
|
83
|
+
correlationId: input.correlationId,
|
|
84
|
+
correlatedEvents,
|
|
85
|
+
networkFailure: input.networkFailure,
|
|
86
|
+
responseCharacteristics: input.responseCharacteristics,
|
|
87
|
+
});
|
|
88
|
+
const failed = input.networkFailure || input.status >= 400;
|
|
89
|
+
if (!failed) {
|
|
90
|
+
publish(build(eventsNear(input.startedAt, completedAt)));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Hold a failing request briefly so the console error it triggers lands in
|
|
94
|
+
// the same durable observation instead of a separate, uncorrelated record.
|
|
95
|
+
const held = new Promise(resolve => {
|
|
96
|
+
const finish = () => {
|
|
97
|
+
publish(build(eventsNear(input.startedAt, Date.now())));
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
if (typeof scope.setTimeout === 'function')
|
|
101
|
+
scope.setTimeout(finish, correlationWindowMs);
|
|
102
|
+
else
|
|
103
|
+
finish();
|
|
104
|
+
}).finally(() => pending.delete(held));
|
|
105
|
+
pending.add(held);
|
|
106
|
+
};
|
|
107
|
+
// --- console instrumentation -------------------------------------------
|
|
108
|
+
const originalConsoleError = console.error?.bind(console);
|
|
109
|
+
const originalConsoleWarn = console.warn?.bind(console);
|
|
110
|
+
const describeArguments = (args) => args
|
|
111
|
+
.map(value => {
|
|
112
|
+
if (value instanceof Error)
|
|
113
|
+
return `${value.name}: ${value.message}`;
|
|
114
|
+
if (typeof value === 'string')
|
|
115
|
+
return value;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.stringify(value);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.join(' ');
|
|
124
|
+
if (originalConsoleError) {
|
|
125
|
+
console.error = (...args) => {
|
|
126
|
+
recordEvent({
|
|
127
|
+
kind: 'console_error',
|
|
128
|
+
message: redactMessage(describeArguments(args)),
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
stack: args.find(value => value instanceof Error) instanceof Error
|
|
131
|
+
? redactMessage(String(args.find(value => value instanceof Error).stack || ''))
|
|
132
|
+
: undefined,
|
|
133
|
+
});
|
|
134
|
+
originalConsoleError(...args);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (originalConsoleWarn) {
|
|
138
|
+
console.warn = (...args) => {
|
|
139
|
+
recordEvent({ kind: 'console_warn', message: redactMessage(describeArguments(args)), timestamp: Date.now() });
|
|
140
|
+
originalConsoleWarn(...args);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// --- uncaught error instrumentation ------------------------------------
|
|
144
|
+
const onError = (event) => {
|
|
145
|
+
recordEvent({
|
|
146
|
+
kind: 'runtime_error',
|
|
147
|
+
message: redactMessage(String(event?.message || event?.error?.message || 'Uncaught error')),
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
stack: event?.error?.stack ? redactMessage(String(event.error.stack)) : undefined,
|
|
150
|
+
source: event?.filename ? { file: String(event.filename), line: event.lineno, column: event.colno } : undefined,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const onRejection = (event) => {
|
|
154
|
+
const reason = event?.reason;
|
|
155
|
+
recordEvent({
|
|
156
|
+
kind: 'unhandled_rejection',
|
|
157
|
+
message: redactMessage(reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason)),
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
stack: reason instanceof Error && reason.stack ? redactMessage(String(reason.stack)) : undefined,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
scope.addEventListener?.('error', onError);
|
|
163
|
+
scope.addEventListener?.('unhandledrejection', onRejection);
|
|
164
|
+
// --- fetch instrumentation ---------------------------------------------
|
|
165
|
+
const originalFetch = scope.fetch?.bind(scope);
|
|
166
|
+
if (originalFetch) {
|
|
167
|
+
scope.fetch = async (input, init) => {
|
|
168
|
+
const url = typeof input === 'string' ? input : input?.url ? String(input.url) : String(input);
|
|
169
|
+
const method = String(init?.method || (typeof input === 'object' && input?.method) || 'GET');
|
|
170
|
+
if (isSelfRequest(url))
|
|
171
|
+
return originalFetch(input, init);
|
|
172
|
+
const startedAt = Date.now();
|
|
173
|
+
const correlationId = createCorrelationId();
|
|
174
|
+
try {
|
|
175
|
+
const response = await originalFetch(input, init);
|
|
176
|
+
report({
|
|
177
|
+
method,
|
|
178
|
+
url,
|
|
179
|
+
status: response.status,
|
|
180
|
+
startedAt,
|
|
181
|
+
correlationId,
|
|
182
|
+
responseCharacteristics: {
|
|
183
|
+
ok: response.ok,
|
|
184
|
+
statusText: response.statusText,
|
|
185
|
+
contentType: response.headers?.get?.('content-type') || undefined,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return response;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
report({
|
|
192
|
+
method,
|
|
193
|
+
url,
|
|
194
|
+
status: 0,
|
|
195
|
+
startedAt,
|
|
196
|
+
correlationId,
|
|
197
|
+
networkFailure: true,
|
|
198
|
+
responseCharacteristics: { error: redactMessage(String(error)) },
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// --- XMLHttpRequest instrumentation ------------------------------------
|
|
205
|
+
const OriginalXhr = scope.XMLHttpRequest;
|
|
206
|
+
if (OriginalXhr) {
|
|
207
|
+
const originalOpen = OriginalXhr.prototype.open;
|
|
208
|
+
const originalSend = OriginalXhr.prototype.send;
|
|
209
|
+
OriginalXhr.prototype.open = function (method, url, ...rest) {
|
|
210
|
+
this.__feltdb = { method, url };
|
|
211
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
212
|
+
};
|
|
213
|
+
OriginalXhr.prototype.send = function (...args) {
|
|
214
|
+
const context = this.__feltdb;
|
|
215
|
+
if (context && !isSelfRequest(context.url)) {
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const correlationId = createCorrelationId();
|
|
218
|
+
let settled = false;
|
|
219
|
+
this.addEventListener('loadend', () => {
|
|
220
|
+
if (settled)
|
|
221
|
+
return;
|
|
222
|
+
settled = true;
|
|
223
|
+
const networkFailure = this.status === 0;
|
|
224
|
+
report({
|
|
225
|
+
method: context.method,
|
|
226
|
+
url: context.url,
|
|
227
|
+
status: this.status,
|
|
228
|
+
startedAt,
|
|
229
|
+
correlationId,
|
|
230
|
+
networkFailure,
|
|
231
|
+
responseCharacteristics: { statusText: this.statusText || undefined },
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return originalSend.apply(this, args);
|
|
236
|
+
};
|
|
237
|
+
OriginalXhr.__feltdbOriginalOpen = originalOpen;
|
|
238
|
+
OriginalXhr.__feltdbOriginalSend = originalSend;
|
|
239
|
+
}
|
|
240
|
+
const flush = async () => {
|
|
241
|
+
// Held observations register new promises as they settle, so drain until
|
|
242
|
+
// the set is genuinely empty rather than snapshotting once.
|
|
243
|
+
while (pending.size > 0)
|
|
244
|
+
await Promise.all([...pending]);
|
|
245
|
+
};
|
|
246
|
+
const handle = {
|
|
247
|
+
workspaceId: connection.workspaceId,
|
|
248
|
+
sessionId: options.sessionId,
|
|
249
|
+
runtimeInstanceId: options.runtimeInstanceId,
|
|
250
|
+
observations: () => [...emitted],
|
|
251
|
+
flush,
|
|
252
|
+
stop: async () => {
|
|
253
|
+
if (originalFetch)
|
|
254
|
+
scope.fetch = originalFetch;
|
|
255
|
+
if (originalConsoleError)
|
|
256
|
+
console.error = originalConsoleError;
|
|
257
|
+
if (originalConsoleWarn)
|
|
258
|
+
console.warn = originalConsoleWarn;
|
|
259
|
+
if (OriginalXhr) {
|
|
260
|
+
OriginalXhr.prototype.open = OriginalXhr.__feltdbOriginalOpen;
|
|
261
|
+
OriginalXhr.prototype.send = OriginalXhr.__feltdbOriginalSend;
|
|
262
|
+
}
|
|
263
|
+
scope.removeEventListener?.('error', onError);
|
|
264
|
+
scope.removeEventListener?.('unhandledrejection', onRejection);
|
|
265
|
+
await flush();
|
|
266
|
+
await connection.disconnect();
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
if (options.exposeGlobal !== false)
|
|
270
|
+
scope.__feltdbRuntime = handle;
|
|
271
|
+
return handle;
|
|
272
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9B,OAAO,CAAC,iBAAiB,CAAC,CAoB5B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAC7F,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;AAsD9H;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,8BAA8B,CAAC,CAwCzC;AAYD;;;;;GAKG;AACH,qBAAa,8BAA8B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,aAAa,CAA4E;IACjG,WAAW,EAAE,MAAM,CAAK;IACxB,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,iBAAiB,CAA6B;gBAGpD,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,0BAA0B;IAQrC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"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"}
|
|
@@ -196,12 +196,21 @@ export class DevelopmentWorkspaceConnection {
|
|
|
196
196
|
async disconnect() {
|
|
197
197
|
this.unsubscribeEvents?.();
|
|
198
198
|
this.unsubscribeEvents = null;
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
try {
|
|
200
|
+
if (this.transport) {
|
|
201
|
+
await this.transport.unregisterClient(this.workspaceId, this.clientId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Disconnecting after the workspace authority has already stopped is
|
|
206
|
+
// normal, not an error: the client registration is gone with it. The
|
|
207
|
+
// local connection must still be torn down.
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
this.subscriptions.clear();
|
|
211
|
+
this.transport = null;
|
|
212
|
+
this.connectedAt = 0;
|
|
201
213
|
}
|
|
202
|
-
this.subscriptions.clear();
|
|
203
|
-
this.transport = null;
|
|
204
|
-
this.connectedAt = 0;
|
|
205
214
|
}
|
|
206
215
|
/**
|
|
207
216
|
* Get capabilities for this client type
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages durable workspace identities and discovery.
|
|
5
5
|
*/
|
|
6
6
|
import type { DevelopmentWorkspace, WorkspaceDiscovery } from './workspace-types.js';
|
|
7
|
-
export declare function createWorkspaceId(
|
|
7
|
+
export declare function createWorkspaceId(_projectId: string): string;
|
|
8
8
|
export declare function isValidWorkspaceId(id: string): boolean;
|
|
9
9
|
export declare function createDevelopmentWorkspace(projectId: string, name: string): DevelopmentWorkspace;
|
|
10
10
|
export declare function updateWorkspaceTimestamp(workspace: DevelopmentWorkspace): DevelopmentWorkspace;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-identity.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-identity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAIrF,wBAAgB,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"workspace-identity.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-identity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAIrF,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,oBAAoB,CAStB;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,oBAAoB,GAC9B,oBAAoB,CAKtB;AAED,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA0BpC;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,kBAAkB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Manages durable workspace identities and discovery.
|
|
5
5
|
*/
|
|
6
6
|
const WORKSPACE_ID_PREFIX = 'ws_';
|
|
7
|
-
export function createWorkspaceId(
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
return `${WORKSPACE_ID_PREFIX}${
|
|
7
|
+
export function createWorkspaceId(_projectId) {
|
|
8
|
+
const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '')
|
|
9
|
+
?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
return `${WORKSPACE_ID_PREFIX}${random}`;
|
|
11
11
|
}
|
|
12
12
|
export function isValidWorkspaceId(id) {
|
|
13
13
|
return id.startsWith(WORKSPACE_ID_PREFIX);
|