@chaos-maker/core 0.3.0 → 0.5.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 +151 -11
- package/dist/chaos-config.schema.json +1314 -0
- package/dist/chaos-config.schema.notes.md +32 -0
- package/dist/chaos-maker.cjs +128 -4
- package/dist/chaos-maker.js +3839 -2017
- package/dist/chaos-maker.umd.js +128 -4
- package/dist/sw.js +1 -0
- package/dist/sw.mjs +1074 -0
- package/dist/types/ChaosMaker.d.ts +60 -1
- package/dist/types/builder.d.ts +37 -1
- package/dist/types/config.d.ts +141 -18
- package/dist/types/debug.d.ts +58 -0
- package/dist/types/errors.d.ts +14 -2
- package/dist/types/events.d.ts +65 -1
- package/dist/types/graphql.d.ts +54 -0
- package/dist/types/groups.d.ts +71 -0
- package/dist/types/index.d.ts +34 -5
- package/dist/types/interceptors/domAssailant.d.ts +2 -1
- package/dist/types/interceptors/eventSource.d.ts +42 -0
- package/dist/types/interceptors/networkFetch.d.ts +2 -1
- package/dist/types/interceptors/networkXHR.d.ts +2 -1
- package/dist/types/interceptors/websocket.d.ts +4 -3
- package/dist/types/presets.d.ts +63 -2
- package/dist/types/sw-bridge-source.d.ts +20 -0
- package/dist/types/sw.d.ts +104 -0
- package/dist/types/transport.d.ts +21 -0
- package/dist/types/utils.d.ts +46 -1
- package/dist/types/validation-deprecation.d.ts +7 -0
- package/dist/types/validation-format.d.ts +10 -0
- package/dist/types/validation-strip.d.ts +11 -0
- package/dist/types/validation-types.d.ts +34 -0
- package/dist/types/validation.d.ts +52 -0
- package/package.json +13 -6
|
@@ -1,26 +1,85 @@
|
|
|
1
1
|
import { ChaosConfig } from './config';
|
|
2
|
+
import { type ValidateChaosConfigOptions } from './validation';
|
|
2
3
|
import { ChaosEvent, ChaosEventType, ChaosEventListener } from './events';
|
|
4
|
+
import { RuleGroup } from './groups';
|
|
5
|
+
/**
|
|
6
|
+
* Global context ChaosMaker patches against. Must expose at minimum `fetch`
|
|
7
|
+
* (network chaos), and optionally `XMLHttpRequest` / `WebSocket`. In a
|
|
8
|
+
* browser page this is `window`; in a service worker / dedicated worker
|
|
9
|
+
* this is `self`. `globalThis` resolves to the correct value in both.
|
|
10
|
+
*/
|
|
11
|
+
export type ChaosTarget = typeof globalThis;
|
|
12
|
+
export interface ChaosMakerOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Explicit global to install chaos on. Defaults to `globalThis`, which
|
|
15
|
+
* resolves correctly in both window and service-worker contexts. Pass
|
|
16
|
+
* `window` or `self` explicitly only for cross-context testing.
|
|
17
|
+
*/
|
|
18
|
+
target?: ChaosTarget;
|
|
19
|
+
/**
|
|
20
|
+
* Forwarded to `validateChaosConfig` during construction. Use to
|
|
21
|
+
* relax unknown-field handling, hook deprecation events, or run custom
|
|
22
|
+
* per-`RuleType` validators.
|
|
23
|
+
*/
|
|
24
|
+
validation?: ValidateChaosConfigOptions;
|
|
25
|
+
}
|
|
3
26
|
export declare class ChaosMaker {
|
|
4
27
|
private config;
|
|
5
28
|
private emitter;
|
|
6
29
|
private random;
|
|
7
30
|
private seed;
|
|
8
31
|
private running;
|
|
32
|
+
private target;
|
|
9
33
|
private originalFetch?;
|
|
10
34
|
private originalXhrSend?;
|
|
11
35
|
private originalXhrOpen?;
|
|
12
36
|
private domObserver?;
|
|
13
37
|
private originalWebSocket?;
|
|
14
38
|
private webSocketHandle?;
|
|
39
|
+
private originalEventSource?;
|
|
40
|
+
private eventSourceHandle?;
|
|
15
41
|
/** Shared counters keyed by config rule object reference. Shared across fetch + XHR + WS. */
|
|
16
42
|
private requestCounters;
|
|
17
|
-
|
|
43
|
+
/** Rule-group registry. Default-on; default group always exists. */
|
|
44
|
+
private groups;
|
|
45
|
+
/** Positional rule-id map shared across interceptors via emitter.
|
|
46
|
+
* Built lazily — only when debug mode is enabled — so disabled instances
|
|
47
|
+
* pay zero allocation cost. The emitter handles `undefined` ruleIds
|
|
48
|
+
* internally via `?.get(rule)`. */
|
|
49
|
+
private ruleIds?;
|
|
50
|
+
/** Logger fed into the emitter; absent ⇒ debug fast-path no-op. */
|
|
51
|
+
private logger?;
|
|
52
|
+
constructor(config: ChaosConfig, options?: ChaosMakerOptions);
|
|
53
|
+
private seedGroupsFromRules;
|
|
54
|
+
/** Compute the set of group names currently referenced by any rule. Used by `removeGroup`. */
|
|
55
|
+
private collectReferencedGroups;
|
|
18
56
|
/** Get the seed used by this instance. Log this on failure to reproduce exact chaos decisions. */
|
|
19
57
|
getSeed(): number;
|
|
20
58
|
on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
|
|
21
59
|
off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
|
|
22
60
|
getLog(): ChaosEvent[];
|
|
23
61
|
clearLog(): void;
|
|
62
|
+
/** Enable a rule group at runtime. Auto-creates the group if unknown.
|
|
63
|
+
* Engine state and per-rule counters are preserved — no restart. */
|
|
64
|
+
enableGroup(name: string): void;
|
|
65
|
+
/** Disable a rule group at runtime. Subsequent matches are skipped
|
|
66
|
+
* and a single `rule-group:gated` event is emitted per cycle. */
|
|
67
|
+
disableGroup(name: string): void;
|
|
68
|
+
/** Pre-register a group (typically used to ship one as initially disabled). */
|
|
69
|
+
createGroup(name: string, opts?: {
|
|
70
|
+
enabled?: boolean;
|
|
71
|
+
}): void;
|
|
72
|
+
/** Remove a group from the registry. Throws when still referenced unless
|
|
73
|
+
* `{ force: true }`. `'default'` cannot be removed. */
|
|
74
|
+
removeGroup(name: string, opts?: {
|
|
75
|
+
force?: boolean;
|
|
76
|
+
}): boolean;
|
|
77
|
+
hasGroup(name: string): boolean;
|
|
78
|
+
/** True iff the named group is currently enabled (auto-creates unknown names). */
|
|
79
|
+
getGroupState(name: string): boolean;
|
|
80
|
+
/** Snapshot of every known group as `{ name: enabled }`. */
|
|
81
|
+
getGroupsSnapshot(): Record<string, boolean>;
|
|
82
|
+
listGroups(): RuleGroup[];
|
|
24
83
|
start(): void;
|
|
25
84
|
stop(): void;
|
|
26
85
|
}
|
package/dist/types/builder.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { ChaosConfig, CorruptionStrategy, RequestCountingOptions, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
|
|
1
|
+
import { ChaosConfig, CorruptionStrategy, GraphQLOperationMatcher, RequestCountingOptions, SSECorruptionStrategy, WebSocketDirection, WebSocketCorruptionStrategy } from './config';
|
|
2
2
|
export declare class ChaosConfigBuilder {
|
|
3
3
|
private config;
|
|
4
|
+
/** Single-shot group name applied to the next rule pushed and then cleared.
|
|
5
|
+
* Sticky semantics intentionally rejected — silent capture of stale groups
|
|
6
|
+
* is harder to debug than the explicit re-chain. */
|
|
7
|
+
private pendingGroup?;
|
|
8
|
+
/** Queued preset names for `.usePreset(...)`. Silently deduped on
|
|
9
|
+
* push. Flushed onto `out.presets` in `.build()` when non-empty. */
|
|
10
|
+
private pendingPresets;
|
|
4
11
|
constructor(initialConfig?: ChaosConfig);
|
|
12
|
+
/** Tag the next rule pushed with this group name.
|
|
13
|
+
* Single-shot: cleared after the next builder method that pushes a rule. */
|
|
14
|
+
inGroup(name: string): this;
|
|
15
|
+
/** Pre-register a group on the config (typically used to ship one as
|
|
16
|
+
* initially disabled). Equivalent to setting `ChaosConfig.groups` directly. */
|
|
17
|
+
defineGroup(name: string, opts?: {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
}): this;
|
|
20
|
+
/** Apply `pendingGroup` (single-shot) to a rule literal before it is pushed.
|
|
21
|
+
* MUST be called at every rule-push site so `.inGroup(...)` is honored. */
|
|
22
|
+
private withGroup;
|
|
5
23
|
failRequests(urlPattern: string, statusCode: number, probability: number, methods?: string[], body?: string, headers?: Record<string, string>, counting?: RequestCountingOptions): this;
|
|
6
24
|
addLatency(urlPattern: string, delayMs: number, probability: number, methods?: string[], counting?: RequestCountingOptions): this;
|
|
7
25
|
abortRequests(urlPattern: string, probability: number, timeout?: number, methods?: string[], counting?: RequestCountingOptions): this;
|
|
@@ -33,7 +51,25 @@ export declare class ChaosConfigBuilder {
|
|
|
33
51
|
}, counting?: RequestCountingOptions): this;
|
|
34
52
|
dropMessagesOnNth(urlPattern: string, direction: WebSocketDirection, n: number): this;
|
|
35
53
|
delayMessagesOnNth(urlPattern: string, direction: WebSocketDirection, delayMs: number, n: number): this;
|
|
54
|
+
/** Fail every GraphQL request matching `operationName`.
|
|
55
|
+
* Defaults `urlPattern` to `'*'`; pass an explicit pattern as the 4th
|
|
56
|
+
* argument to scope to a specific endpoint. */
|
|
57
|
+
failGraphQLOperation(operationName: GraphQLOperationMatcher, statusCode: number, probability: number, urlPattern?: string): this;
|
|
58
|
+
/** Add `delayMs` of latency to every GraphQL request matching `operationName`. */
|
|
59
|
+
delayGraphQLOperation(operationName: GraphQLOperationMatcher, delayMs: number, probability: number, urlPattern?: string): this;
|
|
60
|
+
dropSSE(urlPattern: string, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
61
|
+
delaySSE(urlPattern: string, delayMs: number, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
62
|
+
corruptSSE(urlPattern: string, strategy: SSECorruptionStrategy, probability: number, eventType?: string, counting?: RequestCountingOptions): this;
|
|
63
|
+
closeSSE(urlPattern: string, probability: number, opts?: {
|
|
64
|
+
afterMs?: number;
|
|
65
|
+
}, counting?: RequestCountingOptions): this;
|
|
36
66
|
/** Set the PRNG seed for reproducible chaos. */
|
|
37
67
|
withSeed(seed: number): this;
|
|
68
|
+
/** Toggle Debug Mode on this config. Off by default. */
|
|
69
|
+
withDebug(enabled?: boolean): this;
|
|
70
|
+
/** Queue a preset name to be expanded at engine init.
|
|
71
|
+
* Silently dedups within the builder, preserving insertion order. Empty
|
|
72
|
+
* / whitespace-only names throw, matching the schema and registry rules. */
|
|
73
|
+
usePreset(name: string): this;
|
|
38
74
|
build(): ChaosConfig;
|
|
39
75
|
}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RuleGroupConfig } from './groups';
|
|
2
|
+
import type { PresetConfigSlice } from './presets';
|
|
1
3
|
/** Counting options shared by all network chaos config types.
|
|
2
4
|
* At most one of `onNth`, `everyNth`, or `afterN` may be set on a single rule.
|
|
3
5
|
* Counting is per-rule and shared across fetch + XHR (only increments when a
|
|
@@ -11,37 +13,57 @@ export interface RequestCountingOptions {
|
|
|
11
13
|
everyNth?: number;
|
|
12
14
|
afterN?: number;
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
/** Optional group membership shared by every rule type.
|
|
17
|
+
* Rules without a `group` belong to the implicit `'default'` group, which is
|
|
18
|
+
* always enabled. Toggling a group at runtime via `enableGroup` /
|
|
19
|
+
* `disableGroup` skips its rules without restarting the engine — counters
|
|
20
|
+
* stay intact across toggles. */
|
|
21
|
+
export interface RuleGroupAssignment {
|
|
22
|
+
group?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Match a GraphQL operation by name. Applied AFTER `urlPattern` + `methods`
|
|
25
|
+
* as an additive filter — never a replacement. Matches against:
|
|
26
|
+
* - JSON `operationName` field on POST request bodies, OR
|
|
27
|
+
* - the operation name parsed from the `query` field (e.g. `query GetUser { … }`),
|
|
28
|
+
* - `?operationName=` query parameter for persisted-query GET requests, OR
|
|
29
|
+
* - operation name parsed from `?query=` in GET requests carrying GraphQL text.
|
|
30
|
+
*
|
|
31
|
+
* When the rule has `graphqlOperation` set but the request body cannot be
|
|
32
|
+
* parsed (multipart upload, ReadableStream, binary), the rule is skipped and
|
|
33
|
+
* a diagnostic event is emitted with `applied: false, reason: 'graphql-body-unparseable'`.
|
|
34
|
+
* XHR requests with non-string bodies are treated the same way.
|
|
35
|
+
*
|
|
36
|
+
* - `string` matches the operation name exactly.
|
|
37
|
+
* - `RegExp` matches when `.test(operationName)` returns true.
|
|
38
|
+
*/
|
|
39
|
+
export type GraphQLOperationMatcher = string | RegExp;
|
|
40
|
+
/** Common matcher fields shared by every network chaos rule type. */
|
|
41
|
+
export interface NetworkRuleMatchers {
|
|
15
42
|
urlPattern: string;
|
|
16
43
|
methods?: string[];
|
|
44
|
+
graphqlOperation?: GraphQLOperationMatcher;
|
|
45
|
+
}
|
|
46
|
+
export interface NetworkFailureConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
|
|
17
47
|
statusCode: number;
|
|
18
48
|
probability: number;
|
|
19
49
|
body?: string;
|
|
20
50
|
statusText?: string;
|
|
21
51
|
headers?: Record<string, string>;
|
|
22
52
|
}
|
|
23
|
-
export interface NetworkLatencyConfig extends RequestCountingOptions {
|
|
24
|
-
urlPattern: string;
|
|
25
|
-
methods?: string[];
|
|
53
|
+
export interface NetworkLatencyConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
|
|
26
54
|
delayMs: number;
|
|
27
55
|
probability: number;
|
|
28
56
|
}
|
|
29
|
-
export interface NetworkAbortConfig extends RequestCountingOptions {
|
|
30
|
-
urlPattern: string;
|
|
31
|
-
methods?: string[];
|
|
57
|
+
export interface NetworkAbortConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
|
|
32
58
|
probability: number;
|
|
33
59
|
timeout?: number;
|
|
34
60
|
}
|
|
35
61
|
export type CorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
|
|
36
|
-
export interface NetworkCorruptionConfig extends RequestCountingOptions {
|
|
37
|
-
urlPattern: string;
|
|
38
|
-
methods?: string[];
|
|
62
|
+
export interface NetworkCorruptionConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
|
|
39
63
|
probability: number;
|
|
40
64
|
strategy: CorruptionStrategy;
|
|
41
65
|
}
|
|
42
|
-
export interface NetworkCorsConfig extends RequestCountingOptions {
|
|
43
|
-
urlPattern: string;
|
|
44
|
-
methods?: string[];
|
|
66
|
+
export interface NetworkCorsConfig extends RequestCountingOptions, NetworkRuleMatchers, RuleGroupAssignment {
|
|
45
67
|
probability: number;
|
|
46
68
|
}
|
|
47
69
|
export interface NetworkConfig {
|
|
@@ -51,7 +73,7 @@ export interface NetworkConfig {
|
|
|
51
73
|
corruptions?: NetworkCorruptionConfig[];
|
|
52
74
|
cors?: NetworkCorsConfig[];
|
|
53
75
|
}
|
|
54
|
-
export interface UiAssaultConfig {
|
|
76
|
+
export interface UiAssaultConfig extends RuleGroupAssignment {
|
|
55
77
|
selector: string;
|
|
56
78
|
action: 'disable' | 'hide' | 'remove';
|
|
57
79
|
probability: number;
|
|
@@ -65,12 +87,12 @@ export interface UiConfig {
|
|
|
65
87
|
* - `both` = apply independently in either direction.
|
|
66
88
|
*/
|
|
67
89
|
export type WebSocketDirection = 'inbound' | 'outbound' | 'both';
|
|
68
|
-
export interface WebSocketDropConfig extends RequestCountingOptions {
|
|
90
|
+
export interface WebSocketDropConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
69
91
|
urlPattern: string;
|
|
70
92
|
direction: WebSocketDirection;
|
|
71
93
|
probability: number;
|
|
72
94
|
}
|
|
73
|
-
export interface WebSocketDelayConfig extends RequestCountingOptions {
|
|
95
|
+
export interface WebSocketDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
74
96
|
urlPattern: string;
|
|
75
97
|
direction: WebSocketDirection;
|
|
76
98
|
delayMs: number;
|
|
@@ -83,13 +105,13 @@ export interface WebSocketDelayConfig extends RequestCountingOptions {
|
|
|
83
105
|
* emitted with `applied: false`.
|
|
84
106
|
*/
|
|
85
107
|
export type WebSocketCorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
|
|
86
|
-
export interface WebSocketCorruptConfig extends RequestCountingOptions {
|
|
108
|
+
export interface WebSocketCorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
87
109
|
urlPattern: string;
|
|
88
110
|
direction: WebSocketDirection;
|
|
89
111
|
strategy: WebSocketCorruptionStrategy;
|
|
90
112
|
probability: number;
|
|
91
113
|
}
|
|
92
|
-
export interface WebSocketCloseConfig extends RequestCountingOptions {
|
|
114
|
+
export interface WebSocketCloseConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
93
115
|
urlPattern: string;
|
|
94
116
|
/**
|
|
95
117
|
* WebSocket close code. Must be either `1000` (Normal Closure) or in the
|
|
@@ -113,10 +135,111 @@ export interface WebSocketConfig {
|
|
|
113
135
|
corruptions?: WebSocketCorruptConfig[];
|
|
114
136
|
closes?: WebSocketCloseConfig[];
|
|
115
137
|
}
|
|
138
|
+
/** Strategies for corrupting Server-Sent Event payloads.
|
|
139
|
+
* All four strategies operate on `event.data` (always a string per the SSE
|
|
140
|
+
* spec). Mirrors the fetch / WebSocket corruption shape so the same
|
|
141
|
+
* vocabulary applies across protocols.
|
|
142
|
+
*/
|
|
143
|
+
export type SSECorruptionStrategy = 'truncate' | 'malformed-json' | 'empty' | 'wrong-type';
|
|
144
|
+
/** Filter SSE chaos to a specific event type.
|
|
145
|
+
* - `'message'` (default in the spec) targets unnamed events fired via
|
|
146
|
+
* `onmessage` / `addEventListener('message', …)`.
|
|
147
|
+
* - Any other string targets named events dispatched with `event:` lines.
|
|
148
|
+
* - `'*'` matches every event regardless of name.
|
|
149
|
+
*/
|
|
150
|
+
export type SSEEventTypeMatcher = string | '*';
|
|
151
|
+
export interface SSEDropConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
152
|
+
urlPattern: string;
|
|
153
|
+
eventType?: SSEEventTypeMatcher;
|
|
154
|
+
probability: number;
|
|
155
|
+
}
|
|
156
|
+
export interface SSEDelayConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
157
|
+
urlPattern: string;
|
|
158
|
+
eventType?: SSEEventTypeMatcher;
|
|
159
|
+
delayMs: number;
|
|
160
|
+
probability: number;
|
|
161
|
+
}
|
|
162
|
+
export interface SSECorruptConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
163
|
+
urlPattern: string;
|
|
164
|
+
eventType?: SSEEventTypeMatcher;
|
|
165
|
+
strategy: SSECorruptionStrategy;
|
|
166
|
+
probability: number;
|
|
167
|
+
}
|
|
168
|
+
export interface SSECloseConfig extends RequestCountingOptions, RuleGroupAssignment {
|
|
169
|
+
urlPattern: string;
|
|
170
|
+
/** Delay after `open` before dispatching `error` + closing, in ms. Default 0. */
|
|
171
|
+
afterMs?: number;
|
|
172
|
+
probability: number;
|
|
173
|
+
}
|
|
174
|
+
export interface SSEConfig {
|
|
175
|
+
drops?: SSEDropConfig[];
|
|
176
|
+
delays?: SSEDelayConfig[];
|
|
177
|
+
corruptions?: SSECorruptConfig[];
|
|
178
|
+
closes?: SSECloseConfig[];
|
|
179
|
+
}
|
|
116
180
|
export interface ChaosConfig {
|
|
117
181
|
network?: NetworkConfig;
|
|
118
182
|
ui?: UiConfig;
|
|
119
183
|
websocket?: WebSocketConfig;
|
|
184
|
+
sse?: SSEConfig;
|
|
185
|
+
/**
|
|
186
|
+
* Pre-register rule groups with an explicit initial enabled state.
|
|
187
|
+
*
|
|
188
|
+
* Rules opt into a group by setting `group: 'name'`; groups referenced from
|
|
189
|
+
* rules but not listed here are auto-registered as enabled. Use this field
|
|
190
|
+
* only to ship a group as initially disabled (e.g. `{ name: 'payments',
|
|
191
|
+
* enabled: false }`) or to reserve a group name with no rules attached yet.
|
|
192
|
+
*/
|
|
193
|
+
groups?: RuleGroupConfig[];
|
|
194
|
+
/**
|
|
195
|
+
* Enable Chaos Maker's structured Debug Mode. When `true`, every
|
|
196
|
+
* rule decision emits a `type: 'debug'` event (with `detail.stage`)
|
|
197
|
+
* through the emitter AND mirrors a `[Chaos] <stage> ...` line to
|
|
198
|
+
* `console.debug`. Framework-agnostic — does not touch
|
|
199
|
+
* Playwright/Cypress/Puppeteer/WDIO debug semantics. Defaults to `false`;
|
|
200
|
+
* fast-path no-op when off.
|
|
201
|
+
*
|
|
202
|
+
* Accepts `boolean` for the common case or `{ enabled: boolean }` to match
|
|
203
|
+
* the `DebugOptions` shape that future Debug Mode extensions (`level`,
|
|
204
|
+
* `prefix`, `console`, `sink`) will add. The validator coerces both forms;
|
|
205
|
+
* the runtime normalizes them via `normalizeDebugOption()`.
|
|
206
|
+
*/
|
|
207
|
+
debug?: boolean | {
|
|
208
|
+
enabled: boolean;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Names of presets to expand into this config at engine init.
|
|
212
|
+
* Resolved against the per-instance `PresetRegistry` seeded with built-ins
|
|
213
|
+
* (camelCase names plus the four kebab-case aliases) and any
|
|
214
|
+
* `customPresets` provided alongside this field.
|
|
215
|
+
*
|
|
216
|
+
* Merge semantics: append-only. Each preset's rule arrays concatenate onto
|
|
217
|
+
* the user's rule arrays in the order listed here, preset rules first and
|
|
218
|
+
* user rules last. Duplicate names are silently deduplicated, preserving
|
|
219
|
+
* first occurrence. Unknown names throw `ChaosConfigError` at construction.
|
|
220
|
+
*
|
|
221
|
+
* Preset configs themselves cannot carry `presets` or `customPresets` —
|
|
222
|
+
* dependency chains are out of scope and rejected by the schema.
|
|
223
|
+
*/
|
|
224
|
+
presets?: string[];
|
|
225
|
+
/**
|
|
226
|
+
* Per-instance custom presets registered alongside the built-ins.
|
|
227
|
+
* Each value is a `PresetConfigSlice` (a `ChaosConfig` minus `presets`,
|
|
228
|
+
* `customPresets`, `seed`, and `debug`). Names collide fail-fast against
|
|
229
|
+
* built-ins and against each other — pick a unique label.
|
|
230
|
+
*
|
|
231
|
+
* Custom preset literals stay mutable on input; the engine deep-clones them
|
|
232
|
+
* during expansion, so post-construction tweaks are not observed by the
|
|
233
|
+
* runtime.
|
|
234
|
+
*/
|
|
235
|
+
customPresets?: Record<string, PresetConfigSlice>;
|
|
236
|
+
/**
|
|
237
|
+
* Reserved for forward-compatibility with future shape changes.
|
|
238
|
+
* Defaults to `1`. Unknown values are rejected at validation time with
|
|
239
|
+
* `code: 'unknown_schema_version'`. Omit this field unless a future major
|
|
240
|
+
* release explicitly bumps the supported version.
|
|
241
|
+
*/
|
|
242
|
+
schemaVersion?: 1;
|
|
120
243
|
/**
|
|
121
244
|
* Seed for Chaos Maker's PRNG.
|
|
122
245
|
*
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug Mode.
|
|
3
|
+
*
|
|
4
|
+
* Two-sink logger that fires when `ChaosConfig.debug` is `true`:
|
|
5
|
+
* 1. Structured `type: 'debug'` events through `ChaosEventEmitter` —
|
|
6
|
+
* consumers subscribe via `instance.on('debug', cb)` and switch on
|
|
7
|
+
* `event.detail.stage` for the stage taxonomy.
|
|
8
|
+
* 2. A formatted `[Chaos] <stage> ...` line (or `[Chaos SW] <stage> ...`
|
|
9
|
+
* when the logger targets a Service Worker) to `console.debug`. Hidden
|
|
10
|
+
* by default in CI loggers, visible in browser DevTools.
|
|
11
|
+
*
|
|
12
|
+
* Framework-agnostic: never reads `process.env.DEBUG`, `--debug`, or
|
|
13
|
+
* `localStorage.debug`. The only signal is `ChaosConfig.debug`.
|
|
14
|
+
*/
|
|
15
|
+
import type { ChaosConfig } from './config';
|
|
16
|
+
import type { ChaosDebugStage, ChaosEvent } from './events';
|
|
17
|
+
export type { ChaosDebugStage } from './events';
|
|
18
|
+
export interface DebugOptions {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function normalizeDebugOption(input: boolean | DebugOptions | undefined): DebugOptions;
|
|
22
|
+
/** Identity assigned to every rule object in a config snapshot. */
|
|
23
|
+
export interface RuleIdEntry {
|
|
24
|
+
ruleType: string;
|
|
25
|
+
ruleId: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a positional rule-id map for a config snapshot. IDs are
|
|
29
|
+
* `<ruleType>#<index>` derived from the order rules appear in their array.
|
|
30
|
+
* Reordering rules between runs changes the IDs — acceptable for in-test
|
|
31
|
+
* diagnostics.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildRuleIdMap(config: ChaosConfig): WeakMap<object, RuleIdEntry>;
|
|
34
|
+
/**
|
|
35
|
+
* Build the human-readable body mirrored to `console.debug`. Does NOT include
|
|
36
|
+
* the `[Chaos]` / `[Chaos SW]` prefix — that is owned by `Logger.log()` and
|
|
37
|
+
* varies by target so the two never compose into a doubled prefix.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatDebugMessage(stage: ChaosDebugStage, detail: ChaosEvent['detail']): string;
|
|
40
|
+
export declare class Logger {
|
|
41
|
+
private readonly opts;
|
|
42
|
+
private readonly target;
|
|
43
|
+
constructor(opts: DebugOptions, target?: 'page' | 'sw');
|
|
44
|
+
isEnabled(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Build a `type: 'debug'` event with `detail.stage = stage`, mirror a
|
|
47
|
+
* `[Chaos] ...` (page) or `[Chaos SW] ...` (Service Worker) line to
|
|
48
|
+
* `console.debug`, and return the event for the emitter to fan out. The
|
|
49
|
+
* formatted string is never stored on the event payload.
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the logger was constructed with `enabled: false`.
|
|
52
|
+
* Internal callers (the emitter fast-path) never reach this branch because
|
|
53
|
+
* `ChaosMaker` does not attach a logger when debug is off, but the guard
|
|
54
|
+
* keeps the public `Logger` API consistent with the `DebugOptions.enabled`
|
|
55
|
+
* contract for external consumers who instantiate it directly.
|
|
56
|
+
*/
|
|
57
|
+
log(stage: ChaosDebugStage, detail: ChaosEvent['detail']): ChaosEvent | null;
|
|
58
|
+
}
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import type { ValidationIssue } from './validation-types';
|
|
2
|
+
/** Aggregate validation failure thrown by `validateChaosConfig` /
|
|
3
|
+
* `prepareChaosConfig` / `validateConfig`.
|
|
4
|
+
*
|
|
5
|
+
* Issues are deterministically sorted (by `path` then `code`) before render
|
|
6
|
+
* so CI logs and snapshot tests stay stable across runs. The render is
|
|
7
|
+
* capped at 50 entries with a `... and N more` summary; the full list is
|
|
8
|
+
* always retained on `.issues` for programmatic inspection. */
|
|
1
9
|
export declare class ChaosConfigError extends Error {
|
|
2
|
-
readonly issues:
|
|
3
|
-
constructor(
|
|
10
|
+
readonly issues: ValidationIssue[];
|
|
11
|
+
constructor(input: ValidationIssue[] | string[]);
|
|
12
|
+
/** v0.4.x-shaped string array. Concatenates `path` + `message` per issue
|
|
13
|
+
* in the same sorted order as `.issues`. Use for log greps that already
|
|
14
|
+
* consume the legacy shape; new code should read `.issues` directly. */
|
|
15
|
+
get messages(): string[];
|
|
4
16
|
}
|
package/dist/types/events.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Logger, RuleIdEntry } from './debug';
|
|
2
|
+
export type ChaosEventType = 'network:failure' | 'network:latency' | 'network:abort' | 'network:corruption' | 'network:cors' | 'ui:assault' | 'websocket:drop' | 'websocket:delay' | 'websocket:corrupt' | 'websocket:close' | 'sse:drop' | 'sse:delay' | 'sse:corrupt' | 'sse:close'
|
|
3
|
+
/** Emitted once per `enableGroup()` call. `applied: true`. */
|
|
4
|
+
| 'rule-group:enabled'
|
|
5
|
+
/** Emitted once per `disableGroup()` call. `applied: true`. */
|
|
6
|
+
| 'rule-group:disabled'
|
|
7
|
+
/** Emitted once per group per toggle cycle when a rule is skipped because
|
|
8
|
+
* its group is disabled. Deduped — at most one event per group between
|
|
9
|
+
* toggles to avoid log floods. `applied: false`. */
|
|
10
|
+
| 'rule-group:gated'
|
|
11
|
+
/** Single Debug Mode event type. The concrete stage of the rule
|
|
12
|
+
* decision pipeline lives on `detail.stage`. `applied: false`. */
|
|
13
|
+
| 'debug';
|
|
14
|
+
/** Stage taxonomy. Stable strings used as `detail.stage` on every
|
|
15
|
+
* `type: 'debug'` event. Defined here (not in `debug.ts`) so the event-detail
|
|
16
|
+
* union can reference it without a circular runtime import. */
|
|
17
|
+
export type ChaosDebugStage = 'rule-evaluating' | 'rule-matched' | 'rule-skip-match' | 'rule-skip-counting' | 'rule-skip-group' | 'rule-skip-probability' | 'rule-applied' | 'lifecycle';
|
|
18
|
+
/** Lifecycle phases. Set on `detail.phase` only when
|
|
19
|
+
* `detail.stage === 'lifecycle'`. WS/SSE direction continues to live on
|
|
20
|
+
* the existing `detail.direction` field — `phase` is intentionally
|
|
21
|
+
* lifecycle-only to avoid overloading. */
|
|
22
|
+
export type ChaosLifecyclePhase = 'engine:start' | 'engine:stop' | 'engine:group-toggled' | 'sw:install' | 'sw:config-applied' | 'sw:config-stopped' | 'sw:group-toggled';
|
|
2
23
|
export interface ChaosEvent {
|
|
3
24
|
type: ChaosEventType;
|
|
4
25
|
timestamp: number;
|
|
@@ -20,8 +41,37 @@ export interface ChaosEvent {
|
|
|
20
41
|
closeCode?: number;
|
|
21
42
|
/** WebSocket close reason (for `websocket:close` events). */
|
|
22
43
|
closeReason?: string;
|
|
44
|
+
/** SSE event type (for `sse:*` events). `'message'` is the spec default. */
|
|
45
|
+
eventType?: string;
|
|
46
|
+
/** GraphQL operation name (for `network:*` events when the request was
|
|
47
|
+
* detected as a GraphQL operation). Pivot on this to slice events by
|
|
48
|
+
* operation in dashboards / assertions. */
|
|
49
|
+
operationName?: string;
|
|
23
50
|
/** Reason string for diagnostic `applied: false` events. */
|
|
24
51
|
reason?: string;
|
|
52
|
+
/** Group name (for `rule-group:*` events, and on gated rule diagnostics). */
|
|
53
|
+
groupName?: string;
|
|
54
|
+
/** New state of a group when `stage === 'lifecycle'` and
|
|
55
|
+
* `phase === 'engine:group-toggled' | 'sw:group-toggled'`. Distinguishes
|
|
56
|
+
* enable from disable on the debug stream so consumers don't have to
|
|
57
|
+
* pivot on the parallel `rule-group:enabled` / `rule-group:disabled`
|
|
58
|
+
* emitter events. */
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
/** Concrete stage of a rule's decision pipeline. Set on every
|
|
61
|
+
* `type: 'debug'` event; unset on non-debug events. */
|
|
62
|
+
stage?: ChaosDebugStage;
|
|
63
|
+
/** Lifecycle phase, set only when `stage === 'lifecycle'`. */
|
|
64
|
+
phase?: ChaosLifecyclePhase;
|
|
65
|
+
/** Rule category — `'failure' | 'latency' | 'abort' | ...`. */
|
|
66
|
+
ruleType?: string;
|
|
67
|
+
/** Deterministic identifier for a specific rule WITHIN A SINGLE
|
|
68
|
+
* CONFIG SNAPSHOT. Positional: reordering rules in your config changes
|
|
69
|
+
* the IDs. Sufficient for in-test diagnostic pinpointing in v0.5.0. */
|
|
70
|
+
ruleId?: string;
|
|
71
|
+
/** Optional human label for a rule (future builder field).
|
|
72
|
+
* Reserved so the event shape doesn't churn when the builder later
|
|
73
|
+
* gains `.failRequests({..., name: 'slow-api'})`. */
|
|
74
|
+
ruleName?: string;
|
|
25
75
|
};
|
|
26
76
|
}
|
|
27
77
|
export type ChaosEventListener = (event: ChaosEvent) => void;
|
|
@@ -29,10 +79,24 @@ export declare class ChaosEventEmitter {
|
|
|
29
79
|
private readonly maxLogEntries;
|
|
30
80
|
private listeners;
|
|
31
81
|
private log;
|
|
82
|
+
private logger;
|
|
83
|
+
private ruleIds;
|
|
32
84
|
constructor(maxLogEntries?: number);
|
|
33
85
|
on(type: ChaosEventType | '*', listener: ChaosEventListener): void;
|
|
34
86
|
off(type: ChaosEventType | '*', listener: ChaosEventListener): void;
|
|
35
87
|
emit(event: ChaosEvent): void;
|
|
88
|
+
/** Attach a Debug Mode logger. When unset, `debug()` is a fast-path no-op. */
|
|
89
|
+
setLogger(logger: Logger | undefined): void;
|
|
90
|
+
/** Attach the rule-id map so debug events auto-resolve `ruleType` /
|
|
91
|
+
* `ruleId` from a rule object reference. */
|
|
92
|
+
setRuleIds(map: WeakMap<object, RuleIdEntry> | undefined): void;
|
|
93
|
+
/**
|
|
94
|
+
* Emit a Debug Mode event. Fast-path no-op when no logger is attached —
|
|
95
|
+
* single undefined-check before any allocation. When `rule` is supplied
|
|
96
|
+
* and present in the rule-id map, `detail.ruleType` and `detail.ruleId`
|
|
97
|
+
* are filled in automatically.
|
|
98
|
+
*/
|
|
99
|
+
debug(stage: ChaosDebugStage, detail: ChaosEvent['detail'], rule?: object): void;
|
|
36
100
|
getLog(): ChaosEvent[];
|
|
37
101
|
clearLog(): void;
|
|
38
102
|
private notify;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { GraphQLOperationMatcher } from './config';
|
|
2
|
+
/** Result of attempting to extract a GraphQL operation from a request. */
|
|
3
|
+
export type GraphQLExtractResult = {
|
|
4
|
+
kind: 'not-graphql';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'extracted';
|
|
7
|
+
operationName: string | null;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'unparseable';
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Pull the operation name out of a GraphQL `query` string without bringing in
|
|
13
|
+
* a full GraphQL parser. Matches the first `query|mutation|subscription`
|
|
14
|
+
* keyword followed by an identifier — the spec form for named operations.
|
|
15
|
+
* Anonymous operations (`query { … }`) return `null`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseOperationFromQueryString(query: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Identify a request as GraphQL and extract its operation name.
|
|
20
|
+
*
|
|
21
|
+
* - `kind: 'extracted'` — request is GraphQL; `operationName` may be `null`
|
|
22
|
+
* for anonymous operations.
|
|
23
|
+
* - `kind: 'not-graphql'` — not a GraphQL request (no body, wrong shape).
|
|
24
|
+
* - `kind: 'unparseable'` — looks like it could be GraphQL but the body is in
|
|
25
|
+
* a form we can't read (multipart upload, ReadableStream, binary). Callers
|
|
26
|
+
* that have a `graphqlOperation` matcher must skip the rule and emit a
|
|
27
|
+
* diagnostic event so the user can debug why their rule isn't firing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractGraphQLOperation(method: string, url: string, bodyText: string | null, bodyUnparseable: boolean): GraphQLExtractResult;
|
|
30
|
+
/** Decide whether a rule's `graphqlOperation` matcher accepts the extracted name.
|
|
31
|
+
*
|
|
32
|
+
* Defensive `lastIndex` reset: validation rejects `/g` and `/y` flags up-front,
|
|
33
|
+
* but matchers can also be constructed dynamically (in-page, after deserialization),
|
|
34
|
+
* so reset here too — `RegExp.test()` mutates `lastIndex` for stateful flags
|
|
35
|
+
* and would flap match outcomes across consecutive calls with the same instance.
|
|
36
|
+
*/
|
|
37
|
+
export declare function operationNameMatches(matcher: GraphQLOperationMatcher, operationName: string | null): boolean;
|
|
38
|
+
/** Outcome of evaluating a rule's `graphqlOperation` matcher against a request. */
|
|
39
|
+
export type GraphQLRuleOutcome = {
|
|
40
|
+
kind: 'skip-no-constraint';
|
|
41
|
+
operationName: string | null;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'match';
|
|
44
|
+
operationName: string | null;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'no-match';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'unparseable';
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Evaluate a rule's `graphqlOperation` matcher against the cached extraction.
|
|
52
|
+
* `extract` is computed once per request and reused across all rules.
|
|
53
|
+
*/
|
|
54
|
+
export declare function evaluateGraphQLRule(matcher: GraphQLOperationMatcher | undefined, extract: GraphQLExtractResult): GraphQLRuleOutcome;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Groups — bulk runtime enable/disable for chaos rules.
|
|
3
|
+
*
|
|
4
|
+
* A `RuleGroupRegistry` tracks named groups (default-on) and answers
|
|
5
|
+
* `isActive(name)` from interceptors before they apply chaos. Groups not
|
|
6
|
+
* declared up-front are auto-registered the first time they are referenced
|
|
7
|
+
* (typo surfacing — appears in `list()` / `getSnapshot()`).
|
|
8
|
+
*
|
|
9
|
+
* Toggle is runtime-only: there is no engine restart, so `requestCounters`
|
|
10
|
+
* survive across `setEnabled()` calls.
|
|
11
|
+
*/
|
|
12
|
+
/** Name of the implicit group all rules without an explicit `group` belong to. */
|
|
13
|
+
export declare const DEFAULT_GROUP_NAME = "default";
|
|
14
|
+
/** Public, declarative form accepted on `ChaosConfig.groups`. */
|
|
15
|
+
export interface RuleGroupConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Snapshot record describing a group inside the registry. */
|
|
20
|
+
export interface RuleGroup {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
/** True when registered via `ChaosConfig.groups` or `createGroup()`. False when auto-registered from `isActive()`. */
|
|
24
|
+
explicit: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare class RuleGroupRegistry {
|
|
27
|
+
private groups;
|
|
28
|
+
/**
|
|
29
|
+
* Tracks groups that have already emitted a `rule-group:gated` event since
|
|
30
|
+
* the last toggle. Cleared on every `setEnabled()` so the next toggle cycle
|
|
31
|
+
* gets a fresh diagnostic event without flooding.
|
|
32
|
+
*/
|
|
33
|
+
private gatedEmitted;
|
|
34
|
+
/**
|
|
35
|
+
* Look up an existing group or create a new one.
|
|
36
|
+
*
|
|
37
|
+
* - Implicit (auto-create from `isActive`): `explicit: false`, defaults
|
|
38
|
+
* `enabled: true`.
|
|
39
|
+
* - Explicit (`ChaosConfig.groups` / `createGroup()`): `explicit: true`.
|
|
40
|
+
* When called with both `explicit: true` and `enabled` set, the existing
|
|
41
|
+
* group's `enabled` is overwritten; the explicit form is the source of truth.
|
|
42
|
+
*/
|
|
43
|
+
ensure(name: string, opts?: {
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
explicit?: boolean;
|
|
46
|
+
}): RuleGroup;
|
|
47
|
+
setEnabled(name: string, enabled: boolean): void;
|
|
48
|
+
/**
|
|
49
|
+
* Auto-creates unknown groups on first check (implicit). Rationale:
|
|
50
|
+
* silently returning `true` for unknown names lets typos like
|
|
51
|
+
* `group: 'paymets'` mask chaos as if no group existed; auto-registering
|
|
52
|
+
* surfaces the typo via `list()` / `getSnapshot()` and keeps default-on
|
|
53
|
+
* backward compat.
|
|
54
|
+
*/
|
|
55
|
+
isActive(name: string | undefined): boolean;
|
|
56
|
+
/** True when this group should emit a `rule-group:gated` event right now (first block since last toggle). */
|
|
57
|
+
shouldEmitGated(name: string): boolean;
|
|
58
|
+
has(name: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Remove a group from the registry.
|
|
61
|
+
* - `'default'` cannot be removed (returns `false`).
|
|
62
|
+
* - By default, throws when any rule in `referencedBy` still uses the group.
|
|
63
|
+
* - Pass `{ force: true }` to remove anyway. Subsequent `isActive(name)`
|
|
64
|
+
* calls auto-recreate the group (default-on).
|
|
65
|
+
*/
|
|
66
|
+
remove(name: string, referencedBy: ReadonlySet<string>, opts?: {
|
|
67
|
+
force?: boolean;
|
|
68
|
+
}): boolean;
|
|
69
|
+
list(): RuleGroup[];
|
|
70
|
+
getSnapshot(): Record<string, boolean>;
|
|
71
|
+
}
|