ccqa-tools 1.37.0
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/README.md +177 -0
- package/dist/coverage/collector.cjs +229 -0
- package/dist/coverage/collector.d.cts +183 -0
- package/dist/coverage/collector.d.ts +183 -0
- package/dist/coverage/collector.js +225 -0
- package/dist/coverage/core.cjs +176 -0
- package/dist/coverage/core.d.cts +157 -0
- package/dist/coverage/core.d.ts +157 -0
- package/dist/coverage/core.js +163 -0
- package/dist/coverage/middleware.cjs +161 -0
- package/dist/coverage/middleware.d.cts +23 -0
- package/dist/coverage/middleware.d.ts +23 -0
- package/dist/coverage/middleware.js +158 -0
- package/dist/coverage/next-loader.cjs +158 -0
- package/dist/coverage/next-loader.d.cts +18 -0
- package/dist/coverage/next-loader.d.ts +19 -0
- package/dist/coverage/next-loader.js +158 -0
- package/dist/coverage/next.cjs +101 -0
- package/dist/coverage/next.d.cts +33 -0
- package/dist/coverage/next.d.ts +33 -0
- package/dist/coverage/next.js +100 -0
- package/dist/coverage/register.cjs +741 -0
- package/dist/coverage/register.d.cts +1 -0
- package/dist/coverage/register.d.ts +1 -0
- package/dist/coverage/register.js +716 -0
- package/dist/coverage/slack.cjs +228 -0
- package/dist/coverage/slack.d.cts +47 -0
- package/dist/coverage/slack.d.ts +47 -0
- package/dist/coverage/slack.js +225 -0
- package/dist/coverage/temporal-workflow.cjs +154 -0
- package/dist/coverage/temporal-workflow.d.cts +28 -0
- package/dist/coverage/temporal-workflow.d.ts +28 -0
- package/dist/coverage/temporal-workflow.js +153 -0
- package/dist/coverage/temporal.cjs +253 -0
- package/dist/coverage/temporal.d.cts +40 -0
- package/dist/coverage/temporal.d.ts +40 -0
- package/dist/coverage/temporal.js +250 -0
- package/package.json +95 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
//#region src/coverage/core.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The hot path, and the only module instrumented application code imports.
|
|
4
|
+
*
|
|
5
|
+
* It has **no imports at all**, on purpose. Bundlers pull this file into every
|
|
6
|
+
* layer they build — a Next.js RSC bundle, an SSR bundle, a route-handler
|
|
7
|
+
* bundle, a Temporal workflow sandbox — and each of those rejects a different
|
|
8
|
+
* subset of Node built-ins. `node:http` alone is enough to fail a Next.js
|
|
9
|
+
* webpack build outright. Everything platform-specific lives behind
|
|
10
|
+
* `ccqa-tools/coverage/register`, which only ever loads in real Node.
|
|
11
|
+
*
|
|
12
|
+
* For the same reason the process-wide state hangs off `globalThis` rather than
|
|
13
|
+
* module scope: a bundler produces several copies of this file per process, and
|
|
14
|
+
* a per-copy AsyncLocalStorage would shard attribution across them.
|
|
15
|
+
*/
|
|
16
|
+
/** The slice of AsyncLocalStorage this package needs, minus the import. */
|
|
17
|
+
interface ContextStorage<T> {
|
|
18
|
+
getStore(): T | undefined;
|
|
19
|
+
run<R>(store: T, fn: () => R): R;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What one in-flight execution carries.
|
|
23
|
+
*
|
|
24
|
+
* Exactly one of the two says where its reach goes. `specId` is set when the
|
|
25
|
+
* request itself named a spec. `actor` is set when nothing did, and all that is
|
|
26
|
+
* known is who caused the work and when it was first asked for — enough for the
|
|
27
|
+
* run to decide later, and nothing this process could decide on its own.
|
|
28
|
+
*/
|
|
29
|
+
interface CoverageStore {
|
|
30
|
+
specId?: string;
|
|
31
|
+
actor?: ActorMark;
|
|
32
|
+
files: Set<string>;
|
|
33
|
+
}
|
|
34
|
+
/** Who caused a request, and the instant it arrived. Never interpreted here. */
|
|
35
|
+
interface ActorMark {
|
|
36
|
+
tag: string;
|
|
37
|
+
at: number;
|
|
38
|
+
}
|
|
39
|
+
/** One identity's reach from one instant, as the collector ships it. */
|
|
40
|
+
interface ActorBucket {
|
|
41
|
+
tag: string;
|
|
42
|
+
at: number;
|
|
43
|
+
files: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
interface CoverageRuntime {
|
|
46
|
+
/**
|
|
47
|
+
* Shape version of this object. Two builds of `ccqa-tools` can end up in
|
|
48
|
+
* one process (an app dependency and a hoisted transitive one); the first to
|
|
49
|
+
* install wins and the rest defer to it, so the shape has to be recognisable.
|
|
50
|
+
*/
|
|
51
|
+
readonly protocol: 1;
|
|
52
|
+
readonly als: ContextStorage<CoverageStore>;
|
|
53
|
+
/** specId -> reached file ids. Union is idempotent, so merging is free. */
|
|
54
|
+
readonly buckets: Map<string, Set<string>>;
|
|
55
|
+
/**
|
|
56
|
+
* `<tag> <at>` -> what that identity reached from that instant.
|
|
57
|
+
*
|
|
58
|
+
* Kept apart from `buckets` because nothing here knows which spec they belong
|
|
59
|
+
* to — or whether they belong to one at all. Most of them are other people
|
|
60
|
+
* using the same environment, and the run discards those.
|
|
61
|
+
*/
|
|
62
|
+
readonly actors: Map<string, ActorBucket>;
|
|
63
|
+
/**
|
|
64
|
+
* Files reached at module top level. Kept out of the spec buckets because the
|
|
65
|
+
* first spec to import a module would otherwise own it, making the result
|
|
66
|
+
* depend on spec execution order.
|
|
67
|
+
*/
|
|
68
|
+
readonly boot: Set<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Number of open buckets, spec and identity alike. `record()` returns on zero
|
|
71
|
+
* before touching anything else — the reason instrumentation costs nothing
|
|
72
|
+
* while nothing is being measured, and the reason this package does not need
|
|
73
|
+
* sampling. A field rather than a `.size` sum because this check is the hot
|
|
74
|
+
* path itself.
|
|
75
|
+
*
|
|
76
|
+
* An identity bucket arms it the same way a spec does, so an application with
|
|
77
|
+
* the actor preset installed pays while any identity has acted recently, not
|
|
78
|
+
* only while a spec is running. That is the price of the application not
|
|
79
|
+
* being told which identities matter.
|
|
80
|
+
*/
|
|
81
|
+
active: number;
|
|
82
|
+
/**
|
|
83
|
+
* Executions that ran while a spec was open but outside its async context.
|
|
84
|
+
* A silent gap here would read as "never reached", so it is always counted.
|
|
85
|
+
*/
|
|
86
|
+
unattributed: number;
|
|
87
|
+
/**
|
|
88
|
+
* Files a load hook saw but could not turn into recorded coverage — an
|
|
89
|
+
* undecodable source, or a parse error. Uncounted, this would render
|
|
90
|
+
* identically to "reached by no spec", which is a lie.
|
|
91
|
+
*/
|
|
92
|
+
uninstrumentedFiles: number;
|
|
93
|
+
/**
|
|
94
|
+
* Set when nothing in this process can be instrumented at all, so every file
|
|
95
|
+
* it runs is missing rather than some of them.
|
|
96
|
+
*
|
|
97
|
+
* A flag and not a count, because the failure is the process: counted as one
|
|
98
|
+
* file it reads as a rounding error next to the thousands it actually hides.
|
|
99
|
+
*/
|
|
100
|
+
uninstrumentedProcess: boolean;
|
|
101
|
+
readonly pid: number;
|
|
102
|
+
readonly startedAt: number;
|
|
103
|
+
}
|
|
104
|
+
/** The name instrumented code calls through. Kept short; it appears per module. */
|
|
105
|
+
declare const GLOBAL_RECORD = "__ccqaCoverage";
|
|
106
|
+
declare function getRuntime(): CoverageRuntime | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Installs the process-wide runtime, or returns the one already there.
|
|
109
|
+
* Called by `ccqa-tools/coverage/register`; application code never calls it.
|
|
110
|
+
*/
|
|
111
|
+
declare function installRuntime(als: ContextStorage<CoverageStore>): CoverageRuntime;
|
|
112
|
+
/**
|
|
113
|
+
* Marks a file as reached. Instrumented code calls this through
|
|
114
|
+
* `globalThis.__ccqaCoverage`, so the call sites cost one global read per
|
|
115
|
+
* module and one truthiness check per invocation when coverage is off.
|
|
116
|
+
*
|
|
117
|
+
* @param topLevel set by the prologue a module runs on first import.
|
|
118
|
+
*/
|
|
119
|
+
declare function record(fileId: string, topLevel?: boolean): void;
|
|
120
|
+
/** The spec the current async context belongs to, if any. */
|
|
121
|
+
declare function currentSpecId(): string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
|
|
124
|
+
* HTTP, Temporal activity, manual — funnels through here.
|
|
125
|
+
*
|
|
126
|
+
* The bucket outlives `fn`: work a request schedules and does not await still
|
|
127
|
+
* belongs to the spec that caused it.
|
|
128
|
+
*/
|
|
129
|
+
declare function runInSpec<R>(specId: string, fn: () => R): R;
|
|
130
|
+
/**
|
|
131
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
132
|
+
*
|
|
133
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
134
|
+
* overwriting it would replace a fact with something the run still has to
|
|
135
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
136
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
137
|
+
* the run discards the ones it did not ask for.
|
|
138
|
+
*/
|
|
139
|
+
declare function runAsActor<R>(tag: string, at: number, fn: () => R): R;
|
|
140
|
+
/** The identity mark on the current async context, if it has one. */
|
|
141
|
+
declare function currentActor(): ActorMark | undefined;
|
|
142
|
+
/** Returns `specId`'s file set, creating it — and arming the gate — if new. */
|
|
143
|
+
declare function openBucket(runtime: CoverageRuntime, specId: string): Set<string>;
|
|
144
|
+
/**
|
|
145
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
146
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
147
|
+
* where the collector tracks both in one map.
|
|
148
|
+
*/
|
|
149
|
+
declare function actorBucketKey(tag: string, at: number): string;
|
|
150
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
151
|
+
declare function openActorBucket(runtime: CoverageRuntime, tag: string, at: number): Set<string>;
|
|
152
|
+
/** Drops a spec's bucket once it has been handed to the collector. */
|
|
153
|
+
declare function closeBucket(runtime: CoverageRuntime, specId: string): void;
|
|
154
|
+
/** Drops an identity's bucket once it has been handed to the collector. */
|
|
155
|
+
declare function closeActorBucket(runtime: CoverageRuntime, key: string): void;
|
|
156
|
+
//#endregion
|
|
157
|
+
export { ActorBucket, ActorMark, ContextStorage, CoverageRuntime, CoverageStore, GLOBAL_RECORD, actorBucketKey, closeActorBucket, closeBucket, currentActor, currentSpecId, getRuntime, installRuntime, openActorBucket, openBucket, record, runAsActor, runInSpec };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
//#region src/coverage/core.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The hot path, and the only module instrumented application code imports.
|
|
4
|
+
*
|
|
5
|
+
* It has **no imports at all**, on purpose. Bundlers pull this file into every
|
|
6
|
+
* layer they build — a Next.js RSC bundle, an SSR bundle, a route-handler
|
|
7
|
+
* bundle, a Temporal workflow sandbox — and each of those rejects a different
|
|
8
|
+
* subset of Node built-ins. `node:http` alone is enough to fail a Next.js
|
|
9
|
+
* webpack build outright. Everything platform-specific lives behind
|
|
10
|
+
* `ccqa-tools/coverage/register`, which only ever loads in real Node.
|
|
11
|
+
*
|
|
12
|
+
* For the same reason the process-wide state hangs off `globalThis` rather than
|
|
13
|
+
* module scope: a bundler produces several copies of this file per process, and
|
|
14
|
+
* a per-copy AsyncLocalStorage would shard attribution across them.
|
|
15
|
+
*/
|
|
16
|
+
/** The slice of AsyncLocalStorage this package needs, minus the import. */
|
|
17
|
+
interface ContextStorage<T> {
|
|
18
|
+
getStore(): T | undefined;
|
|
19
|
+
run<R>(store: T, fn: () => R): R;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What one in-flight execution carries.
|
|
23
|
+
*
|
|
24
|
+
* Exactly one of the two says where its reach goes. `specId` is set when the
|
|
25
|
+
* request itself named a spec. `actor` is set when nothing did, and all that is
|
|
26
|
+
* known is who caused the work and when it was first asked for — enough for the
|
|
27
|
+
* run to decide later, and nothing this process could decide on its own.
|
|
28
|
+
*/
|
|
29
|
+
interface CoverageStore {
|
|
30
|
+
specId?: string;
|
|
31
|
+
actor?: ActorMark;
|
|
32
|
+
files: Set<string>;
|
|
33
|
+
}
|
|
34
|
+
/** Who caused a request, and the instant it arrived. Never interpreted here. */
|
|
35
|
+
interface ActorMark {
|
|
36
|
+
tag: string;
|
|
37
|
+
at: number;
|
|
38
|
+
}
|
|
39
|
+
/** One identity's reach from one instant, as the collector ships it. */
|
|
40
|
+
interface ActorBucket {
|
|
41
|
+
tag: string;
|
|
42
|
+
at: number;
|
|
43
|
+
files: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
interface CoverageRuntime {
|
|
46
|
+
/**
|
|
47
|
+
* Shape version of this object. Two builds of `ccqa-tools` can end up in
|
|
48
|
+
* one process (an app dependency and a hoisted transitive one); the first to
|
|
49
|
+
* install wins and the rest defer to it, so the shape has to be recognisable.
|
|
50
|
+
*/
|
|
51
|
+
readonly protocol: 1;
|
|
52
|
+
readonly als: ContextStorage<CoverageStore>;
|
|
53
|
+
/** specId -> reached file ids. Union is idempotent, so merging is free. */
|
|
54
|
+
readonly buckets: Map<string, Set<string>>;
|
|
55
|
+
/**
|
|
56
|
+
* `<tag> <at>` -> what that identity reached from that instant.
|
|
57
|
+
*
|
|
58
|
+
* Kept apart from `buckets` because nothing here knows which spec they belong
|
|
59
|
+
* to — or whether they belong to one at all. Most of them are other people
|
|
60
|
+
* using the same environment, and the run discards those.
|
|
61
|
+
*/
|
|
62
|
+
readonly actors: Map<string, ActorBucket>;
|
|
63
|
+
/**
|
|
64
|
+
* Files reached at module top level. Kept out of the spec buckets because the
|
|
65
|
+
* first spec to import a module would otherwise own it, making the result
|
|
66
|
+
* depend on spec execution order.
|
|
67
|
+
*/
|
|
68
|
+
readonly boot: Set<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Number of open buckets, spec and identity alike. `record()` returns on zero
|
|
71
|
+
* before touching anything else — the reason instrumentation costs nothing
|
|
72
|
+
* while nothing is being measured, and the reason this package does not need
|
|
73
|
+
* sampling. A field rather than a `.size` sum because this check is the hot
|
|
74
|
+
* path itself.
|
|
75
|
+
*
|
|
76
|
+
* An identity bucket arms it the same way a spec does, so an application with
|
|
77
|
+
* the actor preset installed pays while any identity has acted recently, not
|
|
78
|
+
* only while a spec is running. That is the price of the application not
|
|
79
|
+
* being told which identities matter.
|
|
80
|
+
*/
|
|
81
|
+
active: number;
|
|
82
|
+
/**
|
|
83
|
+
* Executions that ran while a spec was open but outside its async context.
|
|
84
|
+
* A silent gap here would read as "never reached", so it is always counted.
|
|
85
|
+
*/
|
|
86
|
+
unattributed: number;
|
|
87
|
+
/**
|
|
88
|
+
* Files a load hook saw but could not turn into recorded coverage — an
|
|
89
|
+
* undecodable source, or a parse error. Uncounted, this would render
|
|
90
|
+
* identically to "reached by no spec", which is a lie.
|
|
91
|
+
*/
|
|
92
|
+
uninstrumentedFiles: number;
|
|
93
|
+
/**
|
|
94
|
+
* Set when nothing in this process can be instrumented at all, so every file
|
|
95
|
+
* it runs is missing rather than some of them.
|
|
96
|
+
*
|
|
97
|
+
* A flag and not a count, because the failure is the process: counted as one
|
|
98
|
+
* file it reads as a rounding error next to the thousands it actually hides.
|
|
99
|
+
*/
|
|
100
|
+
uninstrumentedProcess: boolean;
|
|
101
|
+
readonly pid: number;
|
|
102
|
+
readonly startedAt: number;
|
|
103
|
+
}
|
|
104
|
+
/** The name instrumented code calls through. Kept short; it appears per module. */
|
|
105
|
+
declare const GLOBAL_RECORD = "__ccqaCoverage";
|
|
106
|
+
declare function getRuntime(): CoverageRuntime | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Installs the process-wide runtime, or returns the one already there.
|
|
109
|
+
* Called by `ccqa-tools/coverage/register`; application code never calls it.
|
|
110
|
+
*/
|
|
111
|
+
declare function installRuntime(als: ContextStorage<CoverageStore>): CoverageRuntime;
|
|
112
|
+
/**
|
|
113
|
+
* Marks a file as reached. Instrumented code calls this through
|
|
114
|
+
* `globalThis.__ccqaCoverage`, so the call sites cost one global read per
|
|
115
|
+
* module and one truthiness check per invocation when coverage is off.
|
|
116
|
+
*
|
|
117
|
+
* @param topLevel set by the prologue a module runs on first import.
|
|
118
|
+
*/
|
|
119
|
+
declare function record(fileId: string, topLevel?: boolean): void;
|
|
120
|
+
/** The spec the current async context belongs to, if any. */
|
|
121
|
+
declare function currentSpecId(): string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
|
|
124
|
+
* HTTP, Temporal activity, manual — funnels through here.
|
|
125
|
+
*
|
|
126
|
+
* The bucket outlives `fn`: work a request schedules and does not await still
|
|
127
|
+
* belongs to the spec that caused it.
|
|
128
|
+
*/
|
|
129
|
+
declare function runInSpec<R>(specId: string, fn: () => R): R;
|
|
130
|
+
/**
|
|
131
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
132
|
+
*
|
|
133
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
134
|
+
* overwriting it would replace a fact with something the run still has to
|
|
135
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
136
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
137
|
+
* the run discards the ones it did not ask for.
|
|
138
|
+
*/
|
|
139
|
+
declare function runAsActor<R>(tag: string, at: number, fn: () => R): R;
|
|
140
|
+
/** The identity mark on the current async context, if it has one. */
|
|
141
|
+
declare function currentActor(): ActorMark | undefined;
|
|
142
|
+
/** Returns `specId`'s file set, creating it — and arming the gate — if new. */
|
|
143
|
+
declare function openBucket(runtime: CoverageRuntime, specId: string): Set<string>;
|
|
144
|
+
/**
|
|
145
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
146
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
147
|
+
* where the collector tracks both in one map.
|
|
148
|
+
*/
|
|
149
|
+
declare function actorBucketKey(tag: string, at: number): string;
|
|
150
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
151
|
+
declare function openActorBucket(runtime: CoverageRuntime, tag: string, at: number): Set<string>;
|
|
152
|
+
/** Drops a spec's bucket once it has been handed to the collector. */
|
|
153
|
+
declare function closeBucket(runtime: CoverageRuntime, specId: string): void;
|
|
154
|
+
/** Drops an identity's bucket once it has been handed to the collector. */
|
|
155
|
+
declare function closeActorBucket(runtime: CoverageRuntime, key: string): void;
|
|
156
|
+
//#endregion
|
|
157
|
+
export { ActorBucket, ActorMark, ContextStorage, CoverageRuntime, CoverageStore, GLOBAL_RECORD, actorBucketKey, closeActorBucket, closeBucket, currentActor, currentSpecId, getRuntime, installRuntime, openActorBucket, openBucket, record, runAsActor, runInSpec };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
//#region src/coverage/core.ts
|
|
2
|
+
const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
|
|
3
|
+
/** The name instrumented code calls through. Kept short; it appears per module. */
|
|
4
|
+
const GLOBAL_RECORD = "__ccqaCoverage";
|
|
5
|
+
function globals() {
|
|
6
|
+
return globalThis;
|
|
7
|
+
}
|
|
8
|
+
function getRuntime() {
|
|
9
|
+
return globals()[RUNTIME_KEY];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Memoized so the hot path skips the `globalThis` read. `installRuntime`
|
|
13
|
+
* primes it and never replaces an installed runtime, so once set it never
|
|
14
|
+
* goes stale — but it must stay undefined (and keep re-reading `globalThis`)
|
|
15
|
+
* until then, or a module that finished loading before `register` installs
|
|
16
|
+
* the runtime would be stuck uninstrumented for the rest of the process.
|
|
17
|
+
*/
|
|
18
|
+
let cachedRuntime;
|
|
19
|
+
function runtime() {
|
|
20
|
+
if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
|
|
21
|
+
return cachedRuntime;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Installs the process-wide runtime, or returns the one already there.
|
|
25
|
+
* Called by `ccqa-tools/coverage/register`; application code never calls it.
|
|
26
|
+
*/
|
|
27
|
+
function installRuntime(als) {
|
|
28
|
+
const g = globals();
|
|
29
|
+
const existing = g[RUNTIME_KEY];
|
|
30
|
+
if (existing) {
|
|
31
|
+
cachedRuntime = existing;
|
|
32
|
+
return existing;
|
|
33
|
+
}
|
|
34
|
+
const created = {
|
|
35
|
+
protocol: 1,
|
|
36
|
+
als,
|
|
37
|
+
buckets: /* @__PURE__ */ new Map(),
|
|
38
|
+
actors: /* @__PURE__ */ new Map(),
|
|
39
|
+
boot: /* @__PURE__ */ new Set(),
|
|
40
|
+
active: 0,
|
|
41
|
+
unattributed: 0,
|
|
42
|
+
uninstrumentedFiles: 0,
|
|
43
|
+
uninstrumentedProcess: false,
|
|
44
|
+
pid: typeof process === "undefined" ? -1 : process.pid,
|
|
45
|
+
startedAt: Date.now()
|
|
46
|
+
};
|
|
47
|
+
g[RUNTIME_KEY] = created;
|
|
48
|
+
g[GLOBAL_RECORD] = record;
|
|
49
|
+
cachedRuntime = created;
|
|
50
|
+
return created;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Marks a file as reached. Instrumented code calls this through
|
|
54
|
+
* `globalThis.__ccqaCoverage`, so the call sites cost one global read per
|
|
55
|
+
* module and one truthiness check per invocation when coverage is off.
|
|
56
|
+
*
|
|
57
|
+
* @param topLevel set by the prologue a module runs on first import.
|
|
58
|
+
*/
|
|
59
|
+
function record(fileId, topLevel) {
|
|
60
|
+
const rt = runtime();
|
|
61
|
+
if (rt === void 0) return;
|
|
62
|
+
if (topLevel === true) {
|
|
63
|
+
rt.boot.add(fileId);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (rt.active === 0) return;
|
|
67
|
+
const store = rt.als.getStore();
|
|
68
|
+
if (store === void 0) {
|
|
69
|
+
rt.unattributed++;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
store.files.add(fileId);
|
|
73
|
+
}
|
|
74
|
+
/** The spec the current async context belongs to, if any. */
|
|
75
|
+
function currentSpecId() {
|
|
76
|
+
return runtime()?.als.getStore()?.specId;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
|
|
80
|
+
* HTTP, Temporal activity, manual — funnels through here.
|
|
81
|
+
*
|
|
82
|
+
* The bucket outlives `fn`: work a request schedules and does not await still
|
|
83
|
+
* belongs to the spec that caused it.
|
|
84
|
+
*/
|
|
85
|
+
function runInSpec(specId, fn) {
|
|
86
|
+
const rt = runtime();
|
|
87
|
+
if (rt === void 0) return fn();
|
|
88
|
+
return rt.als.run({
|
|
89
|
+
specId,
|
|
90
|
+
files: openBucket(rt, specId)
|
|
91
|
+
}, fn);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
95
|
+
*
|
|
96
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
97
|
+
* overwriting it would replace a fact with something the run still has to
|
|
98
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
99
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
100
|
+
* the run discards the ones it did not ask for.
|
|
101
|
+
*/
|
|
102
|
+
function runAsActor(tag, at, fn) {
|
|
103
|
+
const rt = runtime();
|
|
104
|
+
if (rt === void 0) return fn();
|
|
105
|
+
if (rt.als.getStore()?.specId !== void 0) return fn();
|
|
106
|
+
return rt.als.run({
|
|
107
|
+
actor: {
|
|
108
|
+
tag,
|
|
109
|
+
at
|
|
110
|
+
},
|
|
111
|
+
files: openActorBucket(rt, tag, at)
|
|
112
|
+
}, fn);
|
|
113
|
+
}
|
|
114
|
+
/** The identity mark on the current async context, if it has one. */
|
|
115
|
+
function currentActor() {
|
|
116
|
+
return runtime()?.als.getStore()?.actor;
|
|
117
|
+
}
|
|
118
|
+
/** Returns `specId`'s file set, creating it — and arming the gate — if new. */
|
|
119
|
+
function openBucket(runtime, specId) {
|
|
120
|
+
let files = runtime.buckets.get(specId);
|
|
121
|
+
if (files === void 0) {
|
|
122
|
+
files = /* @__PURE__ */ new Set();
|
|
123
|
+
runtime.buckets.set(specId, files);
|
|
124
|
+
armGate(runtime);
|
|
125
|
+
}
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
130
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
131
|
+
* where the collector tracks both in one map.
|
|
132
|
+
*/
|
|
133
|
+
function actorBucketKey(tag, at) {
|
|
134
|
+
return `${tag} ${at}`;
|
|
135
|
+
}
|
|
136
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
137
|
+
function openActorBucket(runtime, tag, at) {
|
|
138
|
+
const key = actorBucketKey(tag, at);
|
|
139
|
+
let bucket = runtime.actors.get(key);
|
|
140
|
+
if (bucket === void 0) {
|
|
141
|
+
bucket = {
|
|
142
|
+
tag,
|
|
143
|
+
at,
|
|
144
|
+
files: /* @__PURE__ */ new Set()
|
|
145
|
+
};
|
|
146
|
+
runtime.actors.set(key, bucket);
|
|
147
|
+
armGate(runtime);
|
|
148
|
+
}
|
|
149
|
+
return bucket.files;
|
|
150
|
+
}
|
|
151
|
+
/** Drops a spec's bucket once it has been handed to the collector. */
|
|
152
|
+
function closeBucket(runtime, specId) {
|
|
153
|
+
if (runtime.buckets.delete(specId)) armGate(runtime);
|
|
154
|
+
}
|
|
155
|
+
/** Drops an identity's bucket once it has been handed to the collector. */
|
|
156
|
+
function closeActorBucket(runtime, key) {
|
|
157
|
+
if (runtime.actors.delete(key)) armGate(runtime);
|
|
158
|
+
}
|
|
159
|
+
function armGate(runtime) {
|
|
160
|
+
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
export { GLOBAL_RECORD, actorBucketKey, closeActorBucket, closeBucket, currentActor, currentSpecId, getRuntime, installRuntime, openActorBucket, openBucket, record, runAsActor, runInSpec };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/coverage/core.ts
|
|
3
|
+
const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
|
|
4
|
+
function globals() {
|
|
5
|
+
return globalThis;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Memoized so the hot path skips the `globalThis` read. `installRuntime`
|
|
9
|
+
* primes it and never replaces an installed runtime, so once set it never
|
|
10
|
+
* goes stale — but it must stay undefined (and keep re-reading `globalThis`)
|
|
11
|
+
* until then, or a module that finished loading before `register` installs
|
|
12
|
+
* the runtime would be stuck uninstrumented for the rest of the process.
|
|
13
|
+
*/
|
|
14
|
+
let cachedRuntime;
|
|
15
|
+
function runtime() {
|
|
16
|
+
if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
|
|
17
|
+
return cachedRuntime;
|
|
18
|
+
}
|
|
19
|
+
/** The spec the current async context belongs to, if any. */
|
|
20
|
+
function currentSpecId() {
|
|
21
|
+
return runtime()?.als.getStore()?.specId;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
|
|
25
|
+
* HTTP, Temporal activity, manual — funnels through here.
|
|
26
|
+
*
|
|
27
|
+
* The bucket outlives `fn`: work a request schedules and does not await still
|
|
28
|
+
* belongs to the spec that caused it.
|
|
29
|
+
*/
|
|
30
|
+
function runInSpec(specId, fn) {
|
|
31
|
+
const rt = runtime();
|
|
32
|
+
if (rt === void 0) return fn();
|
|
33
|
+
return rt.als.run({
|
|
34
|
+
specId,
|
|
35
|
+
files: openBucket(rt, specId)
|
|
36
|
+
}, fn);
|
|
37
|
+
}
|
|
38
|
+
/** Returns `specId`'s file set, creating it — and arming the gate — if new. */
|
|
39
|
+
function openBucket(runtime, specId) {
|
|
40
|
+
let files = runtime.buckets.get(specId);
|
|
41
|
+
if (files === void 0) {
|
|
42
|
+
files = /* @__PURE__ */ new Set();
|
|
43
|
+
runtime.buckets.set(specId, files);
|
|
44
|
+
armGate(runtime);
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
function armGate(runtime) {
|
|
49
|
+
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/coverage/wire.ts
|
|
53
|
+
/**
|
|
54
|
+
* The names a spec id travels under. Every carrier holds the same value,
|
|
55
|
+
* `<runId>.<specId>`, so a hop between them is a copy and never a translation.
|
|
56
|
+
*
|
|
57
|
+
* Like `core.ts` this file imports nothing: the Temporal workflow sandbox reads
|
|
58
|
+
* it too.
|
|
59
|
+
*/
|
|
60
|
+
/** Set on the browser by ccqa at spec start, scoped to the target origin. */
|
|
61
|
+
const COOKIE_NAME = "__ccqa_coverage";
|
|
62
|
+
/** OTel baggage key, for the hop from the first service to downstream ones. */
|
|
63
|
+
const BAGGAGE_KEY = "ccqa.coverage";
|
|
64
|
+
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
65
|
+
/**
|
|
66
|
+
* Accepts a carrier value only if it looks like an id we wrote.
|
|
67
|
+
*
|
|
68
|
+
* The cookie is client-controlled, so this is the first of two gates: the
|
|
69
|
+
* second is the hub refusing runs it never started.
|
|
70
|
+
*/
|
|
71
|
+
function parseSpecId(raw) {
|
|
72
|
+
if (!raw) return void 0;
|
|
73
|
+
const value = raw.trim();
|
|
74
|
+
if (!SPEC_ID.test(value)) return void 0;
|
|
75
|
+
if (value === "1" || value === "true") return void 0;
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function readCookie(header) {
|
|
79
|
+
return readKeyed(header, COOKIE_NAME, ";");
|
|
80
|
+
}
|
|
81
|
+
/** Pulls our key out of a `baggage` header (W3C: `k=v;props,k2=v2`). */
|
|
82
|
+
function readBaggage(header) {
|
|
83
|
+
return readKeyed(header, BAGGAGE_KEY, ",", ";");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Both carriers are `key=value` lists; they differ only in what separates the
|
|
87
|
+
* entries, and baggage allowing properties after each value.
|
|
88
|
+
*
|
|
89
|
+
* One function because the decode-and-validate step is the part that matters,
|
|
90
|
+
* and two copies of it would be free to drift into accepting different things.
|
|
91
|
+
*/
|
|
92
|
+
function readKeyed(header, key, between, propertiesAfter) {
|
|
93
|
+
if (!header) return void 0;
|
|
94
|
+
if (header.indexOf(key) < 0) return void 0;
|
|
95
|
+
for (const raw of header.split(between)) {
|
|
96
|
+
const entry = propertiesAfter === void 0 ? raw : raw.split(propertiesAfter)[0] ?? "";
|
|
97
|
+
const eq = entry.indexOf("=");
|
|
98
|
+
if (eq < 0) continue;
|
|
99
|
+
if (entry.slice(0, eq).trim() !== key) continue;
|
|
100
|
+
try {
|
|
101
|
+
return parseSpecId(decodeURIComponent(entry.slice(eq + 1).trim()));
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Adds our key to an existing `baggage` header value, replacing any old one. */
|
|
108
|
+
function writeBaggage(existing, specId) {
|
|
109
|
+
const kept = (existing ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0 && !entry.startsWith(`ccqa.coverage=`));
|
|
110
|
+
kept.push(`${BAGGAGE_KEY}=${encodeURIComponent(specId)}`);
|
|
111
|
+
return kept.join(",");
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/coverage/middleware.ts
|
|
115
|
+
/**
|
|
116
|
+
* Entry points for servers `ccqa-tools/coverage/register` cannot wrap on its own:
|
|
117
|
+
* anything that does not receive its requests from `node:http`.
|
|
118
|
+
*
|
|
119
|
+
* A framework running on plain Node needs none of this — the register hook
|
|
120
|
+
* already opened the context before the framework saw the request.
|
|
121
|
+
*/
|
|
122
|
+
/** connect / express / fastify-compat middleware. */
|
|
123
|
+
function coverageMiddleware() {
|
|
124
|
+
return function coverage(request, _response, next) {
|
|
125
|
+
const specId = specIdFromNodeHeaders(request.headers);
|
|
126
|
+
if (specId === void 0) {
|
|
127
|
+
next();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
runInSpec(specId, next);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Wraps a `Request` -> `Response` handler (Hono, Next.js route handlers, workerd). */
|
|
134
|
+
function withCoverage(handler) {
|
|
135
|
+
return function covered(request, ...rest) {
|
|
136
|
+
const specId = readCookie(request.headers.get("cookie")) ?? readBaggage(request.headers.get("baggage"));
|
|
137
|
+
if (specId === void 0) return handler(request, ...rest);
|
|
138
|
+
return runInSpec(specId, () => handler(request, ...rest));
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Converts the cookie into a `baggage` header so downstream services — which
|
|
143
|
+
* never see the browser's cookie jar — inherit the attribution. Call it where
|
|
144
|
+
* the first service fans out, or in an edge middleware that forwards to one.
|
|
145
|
+
*/
|
|
146
|
+
function forwardHeaders(incoming, outgoing) {
|
|
147
|
+
const headers = outgoing ?? new Headers();
|
|
148
|
+
const specId = currentSpecId() ?? readCookie(incoming.get("cookie")) ?? readBaggage(incoming.get("baggage"));
|
|
149
|
+
if (specId !== void 0) headers.set("baggage", writeBaggage(headers.get("baggage") ?? incoming.get("baggage"), specId));
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
function specIdFromNodeHeaders(headers) {
|
|
153
|
+
return readCookie(single(headers.cookie)) ?? readBaggage(single(headers.baggage));
|
|
154
|
+
}
|
|
155
|
+
function single(value) {
|
|
156
|
+
return Array.isArray(value) ? value[0] : value;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
exports.coverageMiddleware = coverageMiddleware;
|
|
160
|
+
exports.forwardHeaders = forwardHeaders;
|
|
161
|
+
exports.withCoverage = withCoverage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/coverage/middleware.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Entry points for servers `ccqa-tools/coverage/register` cannot wrap on its own:
|
|
4
|
+
* anything that does not receive its requests from `node:http`.
|
|
5
|
+
*
|
|
6
|
+
* A framework running on plain Node needs none of this — the register hook
|
|
7
|
+
* already opened the context before the framework saw the request.
|
|
8
|
+
*/
|
|
9
|
+
interface NodeLikeRequest {
|
|
10
|
+
headers: Record<string, string | string[] | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/** connect / express / fastify-compat middleware. */
|
|
13
|
+
declare function coverageMiddleware(): (request: NodeLikeRequest, _response: unknown, next: () => void) => void;
|
|
14
|
+
/** Wraps a `Request` -> `Response` handler (Hono, Next.js route handlers, workerd). */
|
|
15
|
+
declare function withCoverage<A extends unknown[], R>(handler: (request: Request, ...rest: A) => R): (request: Request, ...rest: A) => R;
|
|
16
|
+
/**
|
|
17
|
+
* Converts the cookie into a `baggage` header so downstream services — which
|
|
18
|
+
* never see the browser's cookie jar — inherit the attribution. Call it where
|
|
19
|
+
* the first service fans out, or in an edge middleware that forwards to one.
|
|
20
|
+
*/
|
|
21
|
+
declare function forwardHeaders(incoming: Headers, outgoing?: Headers): Headers;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { coverageMiddleware, forwardHeaders, withCoverage };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/coverage/middleware.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Entry points for servers `ccqa-tools/coverage/register` cannot wrap on its own:
|
|
4
|
+
* anything that does not receive its requests from `node:http`.
|
|
5
|
+
*
|
|
6
|
+
* A framework running on plain Node needs none of this — the register hook
|
|
7
|
+
* already opened the context before the framework saw the request.
|
|
8
|
+
*/
|
|
9
|
+
interface NodeLikeRequest {
|
|
10
|
+
headers: Record<string, string | string[] | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/** connect / express / fastify-compat middleware. */
|
|
13
|
+
declare function coverageMiddleware(): (request: NodeLikeRequest, _response: unknown, next: () => void) => void;
|
|
14
|
+
/** Wraps a `Request` -> `Response` handler (Hono, Next.js route handlers, workerd). */
|
|
15
|
+
declare function withCoverage<A extends unknown[], R>(handler: (request: Request, ...rest: A) => R): (request: Request, ...rest: A) => R;
|
|
16
|
+
/**
|
|
17
|
+
* Converts the cookie into a `baggage` header so downstream services — which
|
|
18
|
+
* never see the browser's cookie jar — inherit the attribution. Call it where
|
|
19
|
+
* the first service fans out, or in an edge middleware that forwards to one.
|
|
20
|
+
*/
|
|
21
|
+
declare function forwardHeaders(incoming: Headers, outgoing?: Headers): Headers;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { coverageMiddleware, forwardHeaders, withCoverage };
|