Advanced matchers
Chaos Maker rules historically matched on urlPattern plus optional methods and graphqlOperation. Advanced matchers extend the targeting surface so a single rule can express “the customers API on the production host, GET only, when the bearer token is set, fetch traffic only” without hand-crafting URL substrings. A separate named matcher registry lets multiple rules share one definition.
New matcher fields
Section titled “New matcher fields”Every network rule (failures, latencies, aborts, corruptions, cors) accepts these optional fields alongside the existing ones:
hostname-string | RegExp. String compares case-insensitively againstnew URL(url).hostname. RegExp uses.test(). Thegandyflags are rejected at validation time, matchinggraphqlOperation.queryParams-Record<string, string | RegExp | boolean>. Every entry must pass.truerequires the key to be present (any value).falserequires absence. A string matches the decoded value exactly. A RegExp tests the value.requestHeaders-Record<string, string | RegExp | boolean>. Same value semantics asqueryParams. Key comparison is case-insensitive. The field is namedrequestHeaders(notheaders) so it does not collide with the response-synthesisheadersfield on failure rules.resourceTypes-Array<'fetch' | 'xhr'>. Non-empty. Rule fires only when the originating interceptor is in the list. WebSocket and SSE rules already live in their own categories and are not addressable here.
The matchers are evaluated in order: urlPattern → methods → resourceTypes → hostname → queryParams → requestHeaders → graphqlOperation. The first one that fails skips the rule.
await injectChaos(page, { network: { failures: [ { urlPattern: '/api', hostname: 'api.example.com', methods: ['POST'], queryParams: { tenant: 'acme', debug: false }, requestHeaders: { authorization: /^Bearer / }, resourceTypes: ['fetch'], statusCode: 503, probability: 1, }, ], },});Named matcher registry
Section titled “Named matcher registry”Repeating the same matcher block across multiple rules bloats the config and obscures intent. A named matcher is a reusable bundle stored on the top-level matchers field. Rules reference one by name via the matcher field instead of inlining the matcher fields.
await injectChaos(page, { matchers: { customers: { urlPattern: '/api/customers', hostname: 'api.example.com', methods: ['GET'], }, }, network: { failures: [ { matcher: 'customers', statusCode: 503, probability: 1 }, ], latencies: [ { matcher: 'customers', delayMs: 500, probability: 1 }, ], },});At engine init, the resolver inlines the registered fields into every referencing rule and strips the matchers field from the resolved config. The matcher reference disappears too; downstream interceptors see flat rules identical to the inline form.
Mutual exclusion
Section titled “Mutual exclusion”A rule MUST use either matcher: 'name' alone OR one or more inline matcher fields, never both. Mixing the two surfaces a matcher_inline_conflict validation issue:
// Throws ChaosConfigError with code: 'matcher_inline_conflict'{ matcher: 'customers', urlPattern: '/api/anything', // forbidden alongside `matcher` statusCode: 503, probability: 1,}This keeps the mental model simple: a rule is either a named-matcher reference or a self-contained inline rule. There is no “merge with overrides” precedence to reason about.
Resolution order
Section titled “Resolution order”Resolution runs as the last step inside prepareChaosConfig:
- Zod pass 1 (schema validation).
applyProfileresolvesprofileandprofileOverrides.expandPresetsexpands thepresets[]array.- Zod pass 2 (post-merge re-validation).
resolveNamedMatchersinlinesmatcherreferences against the per-instanceMatcherRegistry.
Running last means rules brought in by presets or profiles can reference matchers defined at the top level. Named matchers themselves cannot reference other matchers; carrying a matcher field inside a registry entry surfaces a matcher_cycle validation issue. This code path is reserved now and will catch real cycles when matcher composition is added in a future release.
Builder method
Section titled “Builder method”The ChaosConfigBuilder exposes .defineMatcher(name, matcher) to register entries fluently:
import { ChaosConfigBuilder } from '@chaos-maker/core';
const config = new ChaosConfigBuilder() .defineMatcher('customers', { urlPattern: '/api/customers', methods: ['GET'] }) .build();Rules that reference the matcher set the matcher field on the literal rule object; the builder rule helpers stay positional and unchanged in this release.
Built-in matchers
Section titled “Built-in matchers”A few matcher definitions get rewritten project after project: the GraphQL endpoint, API traffic, authenticated requests. Chaos Maker ships these as built-in named matchers. A rule references one by name with no matchers entry of its own.
await injectChaos(page, { network: { latencies: [ { matcher: 'graphql', delayMs: 1200, probability: 1 }, ], },});| Name | Targets | Definition |
|---|---|---|
graphql | GraphQL endpoints | urlPattern: '/graphql' |
apiRequests | API traffic | urlPattern: '/api' |
authRequests | Requests carrying an Authorization header | requestHeaders: { authorization: true } |
A built-in resolves through the same path as a user matcher: its fields inline into the rule, the matcher reference is stripped, and debug events carry detail.matcherName. There is no separate runtime and no new rule shape.
Overriding a built-in
Section titled “Overriding a built-in”Declare a matchers entry (or call .defineMatcher) with the same name to replace a built-in for that config. The user entry wins and no collision is raised, so a project can keep the familiar name while pointing it at its own endpoint.
await injectChaos(page, { matchers: { graphql: { urlPattern: '/internal/graphql' }, }, network: { failures: [{ matcher: 'graphql', statusCode: 503, probability: 1 }], },});Built-ins across transports
Section titled “Built-ins across transports”graphql and apiRequests target on urlPattern, so they apply to network, WebSocket, and SSE rules alike.
authRequests is meaningful on network rules only. Its single field, requestHeaders, is not evaluated on WebSocket or SSE rules, because those transports do not expose request headers. A WebSocket or SSE rule that references matcher: 'authRequests' therefore carries no transport-applicable targeting and matches every stream. For WebSocket and SSE, target with graphql, apiRequests, or an inline urlPattern / hostname / queryParams.
Validation issue codes
Section titled “Validation issue codes”Failures surface through the existing ChaosConfigError aggregator. Codes specific to advanced matchers:
matcher_not_found- rule references a name that is not inmatchers(after preset and profile expansion). Path:matchers.<name>.matcher_collision- two entries collide aftertrim()normalization. Path:matchers.<name>.matcher_inline_conflict- rule mixesmatcherwith inline matcher fields. Path:<rule-array>.[<index>].matcher.matcher_cycle- registry entry carries its ownmatcherfield. Reserved for future composition; observable today via untyped configs. Path:matchers.<name>.matcher.
Debug attribution
Section titled “Debug attribution”Rules originating from a named matcher are attributed in debug events. Every type: 'debug' event from a matcher-resolved rule carries detail.matcherName: 'customers' (or whatever name fired). The rule-matched stage additionally carries detail.matchedBy: string[] listing which non-URL matchers fired ('hostname' | 'queryParams' | 'requestHeaders' | 'resourceTypes' | 'graphqlOperation'), and rule-skip-match carries detail.skippedAt: string naming the matcher field that failed.
{ type: 'debug', detail: { stage: 'rule-matched', ruleId: 'failure#0', matcherName: 'customers', matchedBy: ['hostname', 'queryParams'], url: '/api/customers?tenant=acme', method: 'GET', },}This makes the answer to “why did this rule fire?” and “why did this rule skip?” trivially readable in dashboards.
Cross-transport usage
Section titled “Cross-transport usage”WebSocket and SSE rules accept the same named-matcher registry and a subset of the inline matcher fields. The supported inline fields on every WS and SSE rule are:
urlPatternhostnamequeryParamsmatcher(named-matcher reference)
The four network-only fields (methods, requestHeaders, resourceTypes, graphqlOperation) are rejected if inlined onto a WS or SSE rule because they have no meaning on those transports: WebSocket opens via a fixed Upgrade handshake, SSE is GET-only, neither browser API exposes request headers, and there is no JSON-body operation to extract.
A NamedMatcher reused across transports may still declare any of its fields. The WS/SSE gate evaluates only urlPattern, hostname, and queryParams; non-applicable fields are silently ignored so one matcher can target network, WebSocket, and SSE without per-transport duplication.
await injectChaos(page, { matchers: { realtimeApi: { hostname: /realtime/ }, }, websocket: { drops: [ { matcher: 'realtimeApi', direction: 'inbound', probability: 0.2 }, ], }, sse: { drops: [ { matcher: 'realtimeApi', probability: 0.2 }, ], },});Inline forms work the same way:
websocket: { drops: [ { urlPattern: 'wss://realtime', direction: 'outbound', queryParams: { room: 'alpha' }, probability: 1, }, ],}The transport gate runs urlPattern → direction (WS) / eventType (SSE) → hostname → queryParams before counting and probability so a matcher mismatch never consumes counting state. Debug attribution mirrors the network surface: rule-matched carries detail.matchedBy, rule-skip-match carries detail.skippedAt, and matcher-resolved rules carry detail.matcherName.
The TypeScript shape TransportRuleMatchers is a discriminated union: a rule either declares one or more inline fields (urlPattern, hostname, queryParams) or declares matcher: 'name', never both. This mirrors the inline-versus-named split on network rules; urlPattern is optional as long as hostname or queryParams already targets the rule. At runtime, validation rejects rules that supply neither a matcher reference nor at least one inline field, so a rule always carries some targeting.
Parity coverage
Section titled “Parity coverage”Matcher behavior is parity-tested across all four supported adapters.
Playwright, Cypress, WebdriverIO, and Puppeteer each run an identical set of
scenarios from a shared declarative catalog, so a matcher field that fires on
one adapter fires on every adapter, with the same chaos log, the same
matchedBy attribution, and the same observable outcome. The catalog covers
hostname, query parameter, header, and resource-type matching on network
rules, the three built-in matchers and their user-override behavior, and the
WebSocket and SSE matcher subset (urlPattern, hostname, queryParams,
matcher) plus debug matchedBy attribution. New matcher coverage gets
added once to the shared catalog rather than copied into each adapter’s
E2E suite.
Out of scope (deferred)
Section titled “Out of scope (deferred)”- Matcher composition (a
NamedMatcherreferencing another). The error codematcher_cyclereserves the surface for future work. - Response matchers (status code, response headers). Matchers operate on the request only.
- Disjunctive (any-of) matchers. A rule’s matcher block is conjunctive: every field must pass.
See also: Scenario profiles, Presets, Validation.