graphql-watchdog 1.0.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/LICENSE +21 -0
- package/README.md +521 -0
- package/dist/cli.cjs +320 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +332 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1460 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +321 -0
- package/dist/index.d.ts +321 -0
- package/dist/index.js +1438 -0
- package/dist/index.js.map +1 -0
- package/package.json +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Stuart
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# graphql-watchdog
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mstuart/graphql-watchdog/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/graphql-watchdog)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
GraphQL performance toolkit -- N+1 detection, normalized caching, cost analysis, and CI regression testing.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **N+1 Query Detection** -- Automatically detects N+1 patterns in resolver execution and suggests DataLoader fixes
|
|
12
|
+
- **Query Cost Analysis** -- AST-based cost calculation with configurable field costs, list multipliers, and hard limits
|
|
13
|
+
- **Query Optimization Suggestions** -- Analyzes queries and suggests pagination, fragments, DataLoader usage, and more
|
|
14
|
+
- **Dynamic Cost Tracking** -- Automatically derives cost weights from actual resolver performance data
|
|
15
|
+
- **Normalized Response Cache** -- Entity-level caching with LRU eviction, TTL expiration, and type/entity-based invalidation
|
|
16
|
+
- **Pluggable Cache Backends** -- In-memory, Redis, and Cloudflare KV backends via the CacheBackend interface
|
|
17
|
+
- **Performance Dashboard** -- Self-contained HTML dashboard with score gauges, charts, and trend tracking
|
|
18
|
+
- **Server Plugins** -- Drop-in plugins for GraphQL Yoga and Apollo Server
|
|
19
|
+
- **CLI Tooling** -- Static analysis and benchmarking commands for CI/CD pipelines
|
|
20
|
+
- **CI Regression Testing** -- Benchmark operations against endpoints with p50/p95/p99 tracking and regression detection
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install graphql-watchdog graphql
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### With GraphQL Yoga
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createYoga, createSchema } from 'graphql-yoga';
|
|
32
|
+
import { useWatchdog } from 'graphql-watchdog';
|
|
33
|
+
|
|
34
|
+
const yoga = createYoga({
|
|
35
|
+
schema: createSchema({ /* your schema */ }),
|
|
36
|
+
plugins: [
|
|
37
|
+
useWatchdog({
|
|
38
|
+
enableDetector: true,
|
|
39
|
+
enableCost: true,
|
|
40
|
+
cost: {
|
|
41
|
+
maxCost: 1000,
|
|
42
|
+
defaultListMultiplier: 10,
|
|
43
|
+
},
|
|
44
|
+
enableCache: true,
|
|
45
|
+
cache: {
|
|
46
|
+
maxSize: 500,
|
|
47
|
+
ttl: 60000,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### With Apollo Server
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { ApolloServer } from '@apollo/server';
|
|
58
|
+
import { watchdogApolloPlugin } from 'graphql-watchdog';
|
|
59
|
+
|
|
60
|
+
const server = new ApolloServer({
|
|
61
|
+
typeDefs,
|
|
62
|
+
resolvers,
|
|
63
|
+
plugins: [
|
|
64
|
+
watchdogApolloPlugin({
|
|
65
|
+
enableDetector: true,
|
|
66
|
+
onDetection: (detections) => {
|
|
67
|
+
detections.forEach((d) => {
|
|
68
|
+
console.warn(`N+1 detected: ${d.field} (${d.callCount} calls)`);
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Node.js >= 18.0.0
|
|
79
|
+
- `graphql` >= 16.0.0 (peer dependency)
|
|
80
|
+
- `graphql-yoga` >= 5.0.0 (optional, for Yoga plugin)
|
|
81
|
+
- `@apollo/server` >= 4.0.0 (optional, for Apollo plugin)
|
|
82
|
+
- `ioredis` >= 5.0.0 (optional, for Redis cache backend)
|
|
83
|
+
- TypeScript >= 5.0 (optional, for type definitions)
|
|
84
|
+
|
|
85
|
+
Fully written in TypeScript with complete type exports for all public APIs.
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### N+1 Detection
|
|
90
|
+
|
|
91
|
+
The detector instruments resolver functions to track execution patterns and identify N+1 queries:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { ResolverInstrumenter, analyzeForN1 } from 'graphql-watchdog';
|
|
95
|
+
|
|
96
|
+
const instrumenter = new ResolverInstrumenter();
|
|
97
|
+
const instrumented = instrumenter.instrumentResolvers(resolvers);
|
|
98
|
+
|
|
99
|
+
// ... execute GraphQL operations using instrumented resolvers ...
|
|
100
|
+
|
|
101
|
+
const detections = analyzeForN1(instrumenter.getCalls());
|
|
102
|
+
// [{ field: 'Post.author', callCount: 10, severity: 'critical', suggestion: '...' }]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Cost Analysis
|
|
106
|
+
|
|
107
|
+
Analyze query cost statically from the AST:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { analyzeCost, costLimitRule } from 'graphql-watchdog';
|
|
111
|
+
import { parse, validate } from 'graphql';
|
|
112
|
+
|
|
113
|
+
const query = parse(`
|
|
114
|
+
query {
|
|
115
|
+
posts(first: 20) {
|
|
116
|
+
title
|
|
117
|
+
author { name }
|
|
118
|
+
comments(first: 10) { text }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
const breakdown = analyzeCost(query, schema, {
|
|
124
|
+
maxCost: 500,
|
|
125
|
+
defaultListMultiplier: 10,
|
|
126
|
+
costMap: {
|
|
127
|
+
'Query.posts': 2,
|
|
128
|
+
'Post.comments': 5,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(breakdown.totalCost); // calculated cost
|
|
133
|
+
console.log(breakdown.exceeds); // true if over maxCost
|
|
134
|
+
|
|
135
|
+
// Or use as a validation rule
|
|
136
|
+
const errors = validate(schema, query, [costLimitRule(schema, { maxCost: 500 })]);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Query Optimization Suggestions
|
|
140
|
+
|
|
141
|
+
Analyze queries and get actionable optimization suggestions:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { analyzeCost, suggestOptimizations } from 'graphql-watchdog';
|
|
145
|
+
import { parse } from 'graphql';
|
|
146
|
+
|
|
147
|
+
const query = parse(`
|
|
148
|
+
query {
|
|
149
|
+
allUsers {
|
|
150
|
+
name
|
|
151
|
+
posts {
|
|
152
|
+
title
|
|
153
|
+
author { name }
|
|
154
|
+
comments {
|
|
155
|
+
text
|
|
156
|
+
author { name }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
const breakdown = analyzeCost(query, schema);
|
|
164
|
+
const suggestions = suggestOptimizations(breakdown, query, schema);
|
|
165
|
+
|
|
166
|
+
for (const suggestion of suggestions) {
|
|
167
|
+
console.log(`[${suggestion.severity}] ${suggestion.type}: ${suggestion.message}`);
|
|
168
|
+
console.log(` Estimated saving: ${suggestion.estimatedSaving}`);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Suggestion types:
|
|
173
|
+
- **pagination** -- Unbounded list fields missing `first`/`limit` arguments
|
|
174
|
+
- **field-pruning** -- Deeply nested fields contributing disproportionate cost
|
|
175
|
+
- **depth-reduction** -- Queries exceeding 5 levels of nesting
|
|
176
|
+
- **fragment** -- Repeated selection sets that could use fragments
|
|
177
|
+
- **dataloader** -- Object fields under list parents likely causing N+1 queries
|
|
178
|
+
|
|
179
|
+
### Dynamic Cost Tracking
|
|
180
|
+
|
|
181
|
+
Derive cost weights automatically from actual resolver performance:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { DynamicCostTracker, ResolverInstrumenter, analyzeCost } from 'graphql-watchdog';
|
|
185
|
+
|
|
186
|
+
// Create tracker and wire it to the instrumenter
|
|
187
|
+
const tracker = new DynamicCostTracker();
|
|
188
|
+
const instrumenter = new ResolverInstrumenter({ costTracker: tracker });
|
|
189
|
+
const instrumented = instrumenter.instrumentResolvers(resolvers);
|
|
190
|
+
|
|
191
|
+
// ... execute queries -- timing data is recorded automatically ...
|
|
192
|
+
|
|
193
|
+
// Generate cost config from observed performance
|
|
194
|
+
const costConfig = tracker.toCostConfig({
|
|
195
|
+
baselineDuration: 10, // 10ms = cost 1
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Use dynamic costs for analysis
|
|
199
|
+
const breakdown = analyzeCost(query, schema, costConfig);
|
|
200
|
+
|
|
201
|
+
// Export timing data for persistence
|
|
202
|
+
const timingData = tracker.export();
|
|
203
|
+
saveToFile(timingData);
|
|
204
|
+
|
|
205
|
+
// Import on restart
|
|
206
|
+
const saved = loadFromFile();
|
|
207
|
+
tracker.import(saved);
|
|
208
|
+
|
|
209
|
+
// Get stats
|
|
210
|
+
const stats = tracker.getStats();
|
|
211
|
+
console.log(`Tracking ${stats.trackedFields} fields, ${stats.totalCalls} total calls`);
|
|
212
|
+
console.log('Slowest fields:', stats.slowestFields);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Response Cache
|
|
216
|
+
|
|
217
|
+
Normalized caching with automatic invalidation:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { ResponseCache, normalizeResponse, getMutationTypes } from 'graphql-watchdog';
|
|
221
|
+
|
|
222
|
+
const cache = new ResponseCache({
|
|
223
|
+
maxSize: 1000,
|
|
224
|
+
ttl: 60000, // 1 minute
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Cache a response
|
|
228
|
+
const { entities, cacheKey } = normalizeResponse(data, 'GetPosts', variables);
|
|
229
|
+
cache.set(cacheKey, data, entities);
|
|
230
|
+
|
|
231
|
+
// Retrieve from cache
|
|
232
|
+
const cached = cache.get(cacheKey);
|
|
233
|
+
|
|
234
|
+
// Invalidate after mutations
|
|
235
|
+
const affectedTypes = getMutationTypes(mutationDocument, schema);
|
|
236
|
+
affectedTypes.forEach((type) => cache.invalidateByType(type));
|
|
237
|
+
|
|
238
|
+
// Check stats
|
|
239
|
+
const stats = cache.getStats();
|
|
240
|
+
// { hits: 50, misses: 10, hitRate: 0.833, entries: 25 }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Cache Backends
|
|
244
|
+
|
|
245
|
+
The cache supports pluggable backends via the `CacheBackend` interface. The default is in-memory, but you can use Redis or Cloudflare KV.
|
|
246
|
+
|
|
247
|
+
#### Redis Backend
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npm install ioredis # optional peer dependency
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { ResponseCache, RedisCacheBackend } from 'graphql-watchdog';
|
|
255
|
+
|
|
256
|
+
const redisBackend = new RedisCacheBackend({
|
|
257
|
+
url: 'redis://localhost:6379',
|
|
258
|
+
keyPrefix: 'gql-watchdog:',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await redisBackend.connect();
|
|
262
|
+
|
|
263
|
+
const cache = new ResponseCache({
|
|
264
|
+
maxSize: 10000,
|
|
265
|
+
ttl: 300000,
|
|
266
|
+
backend: redisBackend,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Use cache as normal -- data persists in Redis
|
|
270
|
+
cache.set(cacheKey, data, entities);
|
|
271
|
+
|
|
272
|
+
// For backend-backed caches, use getAsync:
|
|
273
|
+
const cached = await cache.getAsync(cacheKey);
|
|
274
|
+
|
|
275
|
+
// Disconnect when done
|
|
276
|
+
await redisBackend.disconnect();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Cloudflare KV Backend
|
|
280
|
+
|
|
281
|
+
No additional dependencies -- uses the Workers KV API available at runtime:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { ResponseCache, CloudflareKVBackend } from 'graphql-watchdog';
|
|
285
|
+
|
|
286
|
+
// In a Cloudflare Worker:
|
|
287
|
+
export default {
|
|
288
|
+
async fetch(request, env) {
|
|
289
|
+
const kvBackend = new CloudflareKVBackend({
|
|
290
|
+
namespace: env.GQL_CACHE, // KV namespace binding
|
|
291
|
+
keyPrefix: 'cache:',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const cache = new ResponseCache({
|
|
295
|
+
ttl: 300000,
|
|
296
|
+
backend: kvBackend,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Use normally
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Custom Backend
|
|
305
|
+
|
|
306
|
+
Implement the `CacheBackend` interface to create your own:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import type { CacheBackend } from 'graphql-watchdog';
|
|
310
|
+
|
|
311
|
+
class MyCustomBackend implements CacheBackend {
|
|
312
|
+
async get(key: string): Promise<string | null> { /* ... */ }
|
|
313
|
+
async set(key: string, value: string, ttlMs?: number): Promise<void> { /* ... */ }
|
|
314
|
+
async del(key: string): Promise<void> { /* ... */ }
|
|
315
|
+
async keys(pattern: string): Promise<string[]> { /* ... */ }
|
|
316
|
+
async delMany(keys: string[]): Promise<number> { /* ... */ }
|
|
317
|
+
async clear(): Promise<void> { /* ... */ }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Performance Dashboard
|
|
322
|
+
|
|
323
|
+
Generate a self-contained HTML performance dashboard:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { generateReport, generateDashboard } from 'graphql-watchdog';
|
|
327
|
+
|
|
328
|
+
// Via generateReport
|
|
329
|
+
const html = generateReport(performanceReport, 'dashboard');
|
|
330
|
+
|
|
331
|
+
// Or directly
|
|
332
|
+
const html = generateDashboard(performanceReport);
|
|
333
|
+
|
|
334
|
+
// Write to file
|
|
335
|
+
import { writeFileSync } from 'fs';
|
|
336
|
+
writeFileSync('dashboard.html', html);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The dashboard includes:
|
|
340
|
+
- **Performance score** (0-100) based on N+1 count, cost, and cache hit rate
|
|
341
|
+
- **N+1 hotspots table** with field, call count, severity, and DataLoader suggestions
|
|
342
|
+
- **Cost breakdown chart** (inline SVG bar chart)
|
|
343
|
+
- **Cache stats gauge** with hit rate, entries, and miss count
|
|
344
|
+
- **Operations table** sorted by duration
|
|
345
|
+
- **localStorage integration** for tracking trends across runs
|
|
346
|
+
- Dark theme, fully self-contained (no external dependencies)
|
|
347
|
+
|
|
348
|
+
### Reporting
|
|
349
|
+
|
|
350
|
+
Generate performance reports in terminal, JSON, or dashboard format:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { generateReport } from 'graphql-watchdog';
|
|
354
|
+
|
|
355
|
+
const report = generateReport(performanceReport, 'terminal'); // colored terminal output
|
|
356
|
+
const json = generateReport(performanceReport, 'json'); // machine-readable JSON
|
|
357
|
+
const html = generateReport(performanceReport, 'dashboard'); // self-contained HTML dashboard
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## CLI
|
|
361
|
+
|
|
362
|
+
### Analyze
|
|
363
|
+
|
|
364
|
+
Run static cost analysis on GraphQL operations:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
graphql-watchdog analyze --schema schema.graphql --operations "queries/**/*.graphql" --max-cost 500
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Options:
|
|
371
|
+
- `--schema <path>` -- Path to GraphQL schema SDL file (required)
|
|
372
|
+
- `--operations <glob>` -- Glob pattern for .graphql operation files (required)
|
|
373
|
+
- `--max-cost <number>` -- Maximum allowed query cost
|
|
374
|
+
- `--default-list-multiplier <number>` -- Default multiplier for list fields
|
|
375
|
+
- `--format <terminal|json>` -- Output format (default: terminal)
|
|
376
|
+
|
|
377
|
+
### Benchmark
|
|
378
|
+
|
|
379
|
+
Benchmark GraphQL operations with regression detection:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
# Run benchmarks
|
|
383
|
+
graphql-watchdog benchmark \
|
|
384
|
+
--endpoint http://localhost:4000/graphql \
|
|
385
|
+
--operations "queries/**/*.graphql" \
|
|
386
|
+
--iterations 50 \
|
|
387
|
+
--output baseline.json
|
|
388
|
+
|
|
389
|
+
# Compare against baseline (exits 1 on regression)
|
|
390
|
+
graphql-watchdog benchmark \
|
|
391
|
+
--endpoint http://localhost:4000/graphql \
|
|
392
|
+
--operations "queries/**/*.graphql" \
|
|
393
|
+
--baseline baseline.json \
|
|
394
|
+
--threshold 20
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Options:
|
|
398
|
+
- `--endpoint <url>` -- GraphQL endpoint URL (required)
|
|
399
|
+
- `--operations <glob>` -- Glob pattern for .graphql files (required)
|
|
400
|
+
- `--baseline <file>` -- Baseline JSON for regression comparison
|
|
401
|
+
- `--iterations <n>` -- Iterations per operation (default: 10)
|
|
402
|
+
- `--output <file>` -- Save results to JSON file
|
|
403
|
+
- `--threshold <percent>` -- Regression threshold % (default: 20)
|
|
404
|
+
|
|
405
|
+
## Comparison with Alternatives
|
|
406
|
+
|
|
407
|
+
graphql-watchdog combines several capabilities that would otherwise require multiple packages:
|
|
408
|
+
|
|
409
|
+
| Feature | graphql-watchdog | graphql-query-complexity | graphql-depth-limit | apollo-server-plugin-response-cache |
|
|
410
|
+
|---------|-----------------|------------------------|--------------------|-------------------------------------|
|
|
411
|
+
| Cost analysis | Yes | Yes | No | No |
|
|
412
|
+
| N+1 detection | Yes | No | No | No |
|
|
413
|
+
| Normalized response cache | Yes | No | No | Yes |
|
|
414
|
+
| Dynamic cost tracking | Yes | No | No | No |
|
|
415
|
+
| Optimization suggestions | Yes | No | No | No |
|
|
416
|
+
| Pluggable cache backends (Redis, CF KV) | Yes | No | No | No |
|
|
417
|
+
| CI benchmark regression testing | Yes | No | No | No |
|
|
418
|
+
| Performance dashboard | Yes | No | No | No |
|
|
419
|
+
| Yoga + Apollo plugins | Yes | Partial | Partial | Apollo only |
|
|
420
|
+
|
|
421
|
+
Choose graphql-watchdog if you want a unified performance toolkit. Choose individual packages if you only need one specific capability.
|
|
422
|
+
|
|
423
|
+
## Configuration Reference
|
|
424
|
+
|
|
425
|
+
### WatchdogConfig
|
|
426
|
+
|
|
427
|
+
| Option | Type | Default | Description |
|
|
428
|
+
|--------|------|---------|-------------|
|
|
429
|
+
| `enableDetector` | `boolean` | `true` | Enable N+1 detection |
|
|
430
|
+
| `enableCost` | `boolean` | `true` | Enable cost analysis |
|
|
431
|
+
| `enableCache` | `boolean` | `false` | Enable response caching |
|
|
432
|
+
| `cost` | `CostConfig` | `{}` | Cost analysis configuration |
|
|
433
|
+
| `cache` | `CacheConfig` | `{}` | Cache configuration |
|
|
434
|
+
| `dynamicCost` | `boolean` | `false` | Enable dynamic cost tracking |
|
|
435
|
+
| `dynamicCostBaseline` | `number` | `10` | Milliseconds per cost unit for dynamic tracking |
|
|
436
|
+
|
|
437
|
+
### CostConfig
|
|
438
|
+
|
|
439
|
+
| Option | Type | Default | Description |
|
|
440
|
+
|--------|------|---------|-------------|
|
|
441
|
+
| `defaultFieldCost` | `number` | `1` | Default cost per field |
|
|
442
|
+
| `defaultListMultiplier` | `number` | `10` | Default multiplier for list fields |
|
|
443
|
+
| `costMap` | `Record<string, number>` | `{}` | Custom costs by `TypeName.fieldName` |
|
|
444
|
+
| `maxCost` | `number` | `Infinity` | Maximum allowed query cost |
|
|
445
|
+
|
|
446
|
+
### CacheConfig
|
|
447
|
+
|
|
448
|
+
| Option | Type | Default | Description |
|
|
449
|
+
|--------|------|---------|-------------|
|
|
450
|
+
| `maxSize` | `number` | `1000` | Maximum cache entries |
|
|
451
|
+
| `ttl` | `number` | `60000` | Time-to-live in milliseconds |
|
|
452
|
+
| `invalidateOnMutation` | `boolean` | `true` | Auto-invalidate on mutations |
|
|
453
|
+
| `backend` | `CacheBackend` | `undefined` | External cache backend (Redis, Cloudflare KV, custom) |
|
|
454
|
+
|
|
455
|
+
## API Reference
|
|
456
|
+
|
|
457
|
+
### Detection
|
|
458
|
+
|
|
459
|
+
- `ResolverInstrumenter` -- Wraps resolvers to track execution
|
|
460
|
+
- `constructor(options?)` -- Optional `{ costTracker: DynamicCostTracker }` for automatic timing
|
|
461
|
+
- `.instrumentResolvers(resolvers)` -- Returns instrumented resolver map
|
|
462
|
+
- `.getCalls()` -- Returns recorded resolver calls
|
|
463
|
+
- `.reset()` -- Clears recorded calls
|
|
464
|
+
- `analyzeForN1(calls, threshold?)` -- Analyzes calls for N+1 patterns
|
|
465
|
+
|
|
466
|
+
### Cost
|
|
467
|
+
|
|
468
|
+
- `analyzeCost(document, schema, config?, variables?)` -- Returns cost breakdown
|
|
469
|
+
- `costLimitRule(schema, config)` -- GraphQL validation rule for cost limits
|
|
470
|
+
- `suggestOptimizations(breakdown, document, schema, config?)` -- Returns optimization suggestions
|
|
471
|
+
- `DynamicCostTracker` -- Tracks resolver performance and generates cost configs
|
|
472
|
+
- `.recordTiming(typeName, fieldName, durationMs)` -- Record a resolver timing
|
|
473
|
+
- `.toCostConfig(options?)` -- Generate CostConfig from observed data
|
|
474
|
+
- `.export()` -- Export timing data for persistence
|
|
475
|
+
- `.import(data)` -- Import previously saved timing data
|
|
476
|
+
- `.getStats()` -- Get summary statistics
|
|
477
|
+
|
|
478
|
+
### Cache
|
|
479
|
+
|
|
480
|
+
- `ResponseCache` -- LRU cache with TTL and entity normalization
|
|
481
|
+
- `.set(key, data, entities)` -- Store response
|
|
482
|
+
- `.get(key)` -- Retrieve response (null if expired/missing)
|
|
483
|
+
- `.getAsync(key)` -- Async retrieve (required for backend-backed caches)
|
|
484
|
+
- `.invalidateByType(typename)` -- Invalidate by type name
|
|
485
|
+
- `.invalidateByEntity(typename, id)` -- Invalidate by specific entity
|
|
486
|
+
- `.getStats()` -- Get hit/miss statistics
|
|
487
|
+
- `.clear()` -- Clear all entries
|
|
488
|
+
- `normalizeResponse(data, operationName, variables?)` -- Normalize response data
|
|
489
|
+
- `getMutationTypes(document, schema)` -- Extract mutation return types
|
|
490
|
+
|
|
491
|
+
### Cache Backends
|
|
492
|
+
|
|
493
|
+
- `CacheBackend` -- Interface for pluggable cache storage
|
|
494
|
+
- `MemoryCacheBackend` -- In-memory implementation with TTL support
|
|
495
|
+
- `RedisCacheBackend` -- Redis backend (requires `ioredis` peer dependency)
|
|
496
|
+
- `.connect()` / `.disconnect()` -- Manage connection lifecycle
|
|
497
|
+
- `CloudflareKVBackend` -- Cloudflare Workers KV backend (no deps needed)
|
|
498
|
+
|
|
499
|
+
### Plugins
|
|
500
|
+
|
|
501
|
+
- `useWatchdog(config?)` -- GraphQL Yoga plugin
|
|
502
|
+
- `watchdogApolloPlugin(config?)` -- Apollo Server plugin
|
|
503
|
+
|
|
504
|
+
### Reporting
|
|
505
|
+
|
|
506
|
+
- `generateReport(report, format?)` -- Generate formatted report (`'terminal'`, `'json'`, or `'dashboard'`)
|
|
507
|
+
- `generateDashboard(report)` -- Generate self-contained HTML dashboard
|
|
508
|
+
- `calculatePerformanceScore(report)` -- Calculate 0-100 performance score
|
|
509
|
+
|
|
510
|
+
## Contributing
|
|
511
|
+
|
|
512
|
+
1. Fork the repository
|
|
513
|
+
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
514
|
+
3. Run tests (`npm test`)
|
|
515
|
+
4. Commit your changes (`git commit -am 'Add my feature'`)
|
|
516
|
+
5. Push to the branch (`git push origin feature/my-feature`)
|
|
517
|
+
6. Open a Pull Request
|
|
518
|
+
|
|
519
|
+
## License
|
|
520
|
+
|
|
521
|
+
MIT
|