@dreki-gg/pi-datadog 0.2.4 → 0.3.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/CHANGELOG.md +6 -0
- package/README.md +46 -3
- package/extensions/datadog/client.ts +14 -6
- package/extensions/datadog/config.ts +20 -0
- package/extensions/datadog/index.ts +164 -0
- package/extensions/datadog/output.ts +5 -2
- package/extensions/datadog/rum-client.ts +130 -0
- package/extensions/datadog/rum-format.ts +188 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @dreki-gg/pi-datadog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `datadog_rum_search` tool for searching Datadog RUM events (sessions, views, actions, front-end errors). Defaults to session events and, like the logs tool, returns a compact inline digest while writing the full untruncated events to a temp file the agent can read. Adds optional `rumApplicationId` / `rumService` fields to `.pi/datadog.json` (with per-call overrides), and surfaces them in the `/datadog` status command.
|
|
8
|
+
|
|
3
9
|
## 0.2.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dreki-gg/pi-datadog
|
|
2
2
|
|
|
3
|
-
Datadog log search tools for [pi](https://github.com/earendil-works/pi) — query production logs with project-aware context.
|
|
3
|
+
Datadog log and RUM search tools for [pi](https://github.com/earendil-works/pi) — query production logs and Real User Monitoring sessions with project-aware context.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -33,7 +33,9 @@ Create `.pi/datadog.json` in your project root to set defaults:
|
|
|
33
33
|
"env": "production",
|
|
34
34
|
"site": "datadoghq.com",
|
|
35
35
|
"defaultTags": ["team:backend"],
|
|
36
|
-
"defaultTimeRange": "1h"
|
|
36
|
+
"defaultTimeRange": "1h",
|
|
37
|
+
"rumApplicationId": "abcd-1234",
|
|
38
|
+
"rumService": "web-frontend"
|
|
37
39
|
}
|
|
38
40
|
```
|
|
39
41
|
|
|
@@ -44,6 +46,8 @@ Create `.pi/datadog.json` in your project root to set defaults:
|
|
|
44
46
|
| `site` | Datadog site | `datadoghq.com` |
|
|
45
47
|
| `defaultTags` | Tags auto-appended to every query | `[]` |
|
|
46
48
|
| `defaultTimeRange` | Default lookback window | `1h` |
|
|
49
|
+
| `rumApplicationId` | Default RUM application id (`@application.id`) for RUM searches | _(none)_ |
|
|
50
|
+
| `rumService` | Default service for RUM searches (falls back to `service`) | _(none)_ |
|
|
47
51
|
|
|
48
52
|
**Supported sites:** `datadoghq.com` (US1), `us3.datadoghq.com`, `us5.datadoghq.com`, `datadoghq.eu` (EU).
|
|
49
53
|
|
|
@@ -61,6 +65,14 @@ Just ask pi to search your logs:
|
|
|
61
65
|
|
|
62
66
|
The agent uses your `.pi/datadog.json` defaults automatically — you don't need to specify service or environment unless you want to override them.
|
|
63
67
|
|
|
68
|
+
You can also ask about front-end / RUM sessions:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
> Show me RUM sessions from the last hour
|
|
72
|
+
> Find RUM errors on the checkout view in production
|
|
73
|
+
> What views did users hit in the last 30 minutes?
|
|
74
|
+
```
|
|
75
|
+
|
|
64
76
|
### Tool: `datadog_logs_search`
|
|
65
77
|
|
|
66
78
|
The agent calls this tool with [Datadog query syntax](https://docs.datadoghq.com/logs/explorer/search_syntax/):
|
|
@@ -75,6 +87,25 @@ The agent calls this tool with [Datadog query syntax](https://docs.datadoghq.com
|
|
|
75
87
|
| `service` | `string` | Override project default service |
|
|
76
88
|
| `env` | `string` | Override project default environment |
|
|
77
89
|
|
|
90
|
+
The agent receives a compact inline digest (counts, status breakdown, services). The full, untruncated log entries are written to a temp file whose path is in the response — the agent reads that file with the `read` tool to inspect actual content.
|
|
91
|
+
|
|
92
|
+
### Tool: `datadog_rum_search`
|
|
93
|
+
|
|
94
|
+
Searches Datadog RUM events (sessions, views, actions, front-end errors) with [RUM query syntax](https://docs.datadoghq.com/real_user_monitoring/explorer/search_syntax/). Defaults to **session** events (`@type:session`) when the query omits `@type`.
|
|
95
|
+
|
|
96
|
+
| Parameter | Type | Description |
|
|
97
|
+
|-----------|------|-------------|
|
|
98
|
+
| `query` | `string` | **Required.** RUM query (e.g. `@type:session`, `@type:error`, `@view.url_path:/checkout`). Leave empty to list recent sessions |
|
|
99
|
+
| `from` | `string` | Start time — relative (`15m`, `1h`, `7d`) or ISO 8601 |
|
|
100
|
+
| `to` | `string` | End time — relative, ISO 8601, or `now` |
|
|
101
|
+
| `limit` | `number` | Max results (1–100, default 25) |
|
|
102
|
+
| `sort` | `string` | `newest` or `oldest` |
|
|
103
|
+
| `service` | `string` | Override project default RUM service |
|
|
104
|
+
| `env` | `string` | Override project default environment |
|
|
105
|
+
| `applicationId` | `string` | Override project default RUM application id (`@application.id`) |
|
|
106
|
+
|
|
107
|
+
Like the logs tool, it returns a compact digest inline (counts, event-type breakdown, services) and writes the full events to a `rum-*.md` temp file the agent can `read`.
|
|
108
|
+
|
|
78
109
|
### Command: `/datadog`
|
|
79
110
|
|
|
80
111
|
Check your configuration and connection status:
|
|
@@ -83,7 +114,7 @@ Check your configuration and connection status:
|
|
|
83
114
|
/datadog
|
|
84
115
|
```
|
|
85
116
|
|
|
86
|
-
Shows: credential status (set/missing), project config (service, env, site, tags).
|
|
117
|
+
Shows: credential status (set/missing), project config (service, env, site, tags, RUM application, RUM service).
|
|
87
118
|
|
|
88
119
|
## Query Syntax Examples
|
|
89
120
|
|
|
@@ -97,6 +128,18 @@ service:my-api env:production @duration:>5000 # Slow requests
|
|
|
97
128
|
|
|
98
129
|
See the [Datadog Log Search Syntax docs](https://docs.datadoghq.com/logs/explorer/search_syntax/) for the full reference.
|
|
99
130
|
|
|
131
|
+
### RUM query examples
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
@type:session # User sessions (the default)
|
|
135
|
+
@type:error # Front-end errors
|
|
136
|
+
@type:view @view.url_path:/checkout # Views on the checkout page
|
|
137
|
+
@type:session @session.type:user # Real user sessions (exclude synthetics)
|
|
138
|
+
@application.id:abcd-1234 @type:action # Actions in a specific RUM application
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See the [RUM Search Syntax docs](https://docs.datadoghq.com/real_user_monitoring/explorer/search_syntax/) for the full reference.
|
|
142
|
+
|
|
100
143
|
## How It Works
|
|
101
144
|
|
|
102
145
|
1. The extension loads `.pi/datadog.json` from your project on session start
|
|
@@ -101,22 +101,30 @@ export function buildQuery(params: LogSearchParams, config: DatadogProjectConfig
|
|
|
101
101
|
/** Max automatic retry attempts on 429 / 5xx responses. */
|
|
102
102
|
const MAX_RETRIES = 4;
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Builds a Datadog SDK configuration with credentials, the target site, and
|
|
106
|
+
* transparent retry on 429 / 5xx. The SDK honours the `x-ratelimit-reset`
|
|
107
|
+
* header, so it waits exactly the window Datadog asks for before retrying
|
|
108
|
+
* (falling back to exponential backoff otherwise). This is what keeps the
|
|
109
|
+
* tools from "crying" about rate limits on bursty usage.
|
|
110
|
+
*
|
|
111
|
+
* Shared by the logs and RUM clients so both inherit identical retry behaviour.
|
|
112
|
+
*/
|
|
113
|
+
export function createConfiguration(credentials: DatadogCredentials, site: string) {
|
|
105
114
|
const configuration = client.createConfiguration({
|
|
106
115
|
authMethods: {
|
|
107
116
|
apiKeyAuth: credentials.apiKey,
|
|
108
117
|
appKeyAuth: credentials.appKey,
|
|
109
118
|
},
|
|
110
|
-
// Transparently retry on 429 / 5xx. The SDK honours the `x-ratelimit-reset`
|
|
111
|
-
// header, so it waits exactly the window Datadog asks for before retrying
|
|
112
|
-
// (falling back to exponential backoff otherwise). This is what keeps the
|
|
113
|
-
// tool from "crying" about rate limits on bursty usage.
|
|
114
119
|
enableRetry: true,
|
|
115
120
|
maxRetries: MAX_RETRIES,
|
|
116
121
|
});
|
|
117
122
|
configuration.setServerVariables({ site });
|
|
123
|
+
return configuration;
|
|
124
|
+
}
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
function createApiInstance(credentials: DatadogCredentials, site: string): v2.LogsApi {
|
|
127
|
+
return new v2.LogsApi(createConfiguration(credentials, site));
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
export interface DatadogErrorInfo {
|
|
@@ -7,6 +7,10 @@ export interface DatadogProjectConfig {
|
|
|
7
7
|
site: string;
|
|
8
8
|
defaultTags?: string[];
|
|
9
9
|
defaultTimeRange: string;
|
|
10
|
+
/** Default RUM application id (@application.id) for RUM event searches. */
|
|
11
|
+
rumApplicationId?: string;
|
|
12
|
+
/** Default service name for RUM event searches (falls back to `service`). */
|
|
13
|
+
rumService?: string;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
interface RawConfig {
|
|
@@ -15,6 +19,8 @@ interface RawConfig {
|
|
|
15
19
|
site?: unknown;
|
|
16
20
|
defaultTags?: unknown;
|
|
17
21
|
defaultTimeRange?: unknown;
|
|
22
|
+
rumApplicationId?: unknown;
|
|
23
|
+
rumService?: unknown;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const DEFAULT_CONFIG: DatadogProjectConfig = {
|
|
@@ -120,5 +126,19 @@ function validateConfig(raw: RawConfig, configPath: string): DatadogProjectConfi
|
|
|
120
126
|
config.defaultTimeRange = raw.defaultTimeRange;
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
if (raw.rumApplicationId !== undefined) {
|
|
130
|
+
if (typeof raw.rumApplicationId !== 'string') {
|
|
131
|
+
throw new Error(`${configPath}: "rumApplicationId" must be a string`);
|
|
132
|
+
}
|
|
133
|
+
config.rumApplicationId = raw.rumApplicationId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (raw.rumService !== undefined) {
|
|
137
|
+
if (typeof raw.rumService !== 'string') {
|
|
138
|
+
throw new Error(`${configPath}: "rumService" must be a string`);
|
|
139
|
+
}
|
|
140
|
+
config.rumService = raw.rumService;
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
return config;
|
|
124
144
|
}
|
|
@@ -8,8 +8,14 @@ import {
|
|
|
8
8
|
loadProjectConfig,
|
|
9
9
|
} from './config.js';
|
|
10
10
|
import { searchLogs, describeDatadogError } from './client.js';
|
|
11
|
+
import { searchRumEvents } from './rum-client.js';
|
|
11
12
|
import { loadDotEnv } from './dotenv.js';
|
|
12
13
|
import { formatSearchResult, formatSearchSummary, formatResultDigest } from './format.js';
|
|
14
|
+
import {
|
|
15
|
+
formatRumSearchResult,
|
|
16
|
+
formatRumSearchSummary,
|
|
17
|
+
formatRumResultDigest,
|
|
18
|
+
} from './rum-format.js';
|
|
13
19
|
import { writeResultsFile } from './output.js';
|
|
14
20
|
|
|
15
21
|
const SORT_ENUM = ['newest', 'oldest'] as const;
|
|
@@ -23,6 +29,14 @@ const TOOL_GUIDELINES = [
|
|
|
23
29
|
"Datadog is rate-limit sensitive: prefer a single narrow, well-scoped `datadog_logs_search` over many broad calls in quick succession. The tool already auto-retries on 429 (honouring Datadog's reset window) — if it still reports a rate limit, wait before retrying rather than firing again immediately.",
|
|
24
30
|
];
|
|
25
31
|
|
|
32
|
+
const RUM_TOOL_GUIDELINES = [
|
|
33
|
+
'Use `datadog_rum_search` to search Datadog RUM (Real User Monitoring) events — user sessions, views, actions, and front-end errors. It uses Datadog RUM query syntax (e.g. `@type:session`, `@type:error`, `@view.url_path:/checkout`, `@session.type:user`).',
|
|
34
|
+
'`datadog_rum_search` defaults to session events (`@type:session`) when your query does not specify `@type`. To inspect other event kinds, include `@type:view`, `@type:action`, or `@type:error` in the query.',
|
|
35
|
+
'`datadog_rum_search` auto-applies project defaults for RUM application, service, and environment from `.pi/datadog.json` — only override when the user explicitly asks for a different application, service, or env.',
|
|
36
|
+
'The inline `datadog_rum_search` output is a compact digest (counts, event-type breakdown, services). The complete events — full attributes, session ids, view URLs — are written to a temp file whose path is in the response. To inspect actual event content, use the `read` tool on that file instead of re-querying.',
|
|
37
|
+
'Use appropriate time ranges with `datadog_rum_search`: "15m" for recent issues, "1h" for general debugging, "24h" or "7d" for trend analysis.',
|
|
38
|
+
];
|
|
39
|
+
|
|
26
40
|
export default function datadogExtension(pi: ExtensionAPI) {
|
|
27
41
|
let projectConfig: DatadogProjectConfig | null = null;
|
|
28
42
|
|
|
@@ -179,6 +193,154 @@ export default function datadogExtension(pi: ExtensionAPI) {
|
|
|
179
193
|
},
|
|
180
194
|
});
|
|
181
195
|
|
|
196
|
+
pi.registerTool({
|
|
197
|
+
name: 'datadog_rum_search',
|
|
198
|
+
label: 'Datadog RUM Search',
|
|
199
|
+
description:
|
|
200
|
+
'Search Datadog RUM events (sessions, views, actions, errors) with RUM query syntax. Defaults to session events and uses project defaults from .pi/datadog.json for RUM application, service, environment, and time range.',
|
|
201
|
+
promptSnippet:
|
|
202
|
+
'Search Datadog RUM events (sessions/views/actions/errors) with project-aware defaults',
|
|
203
|
+
promptGuidelines: RUM_TOOL_GUIDELINES,
|
|
204
|
+
parameters: Type.Object({
|
|
205
|
+
query: Type.String({
|
|
206
|
+
description:
|
|
207
|
+
'Datadog RUM query syntax (e.g. "@type:session", "@type:error", "@view.url_path:/checkout"). Leave empty to list recent sessions.',
|
|
208
|
+
}),
|
|
209
|
+
from: Type.Optional(
|
|
210
|
+
Type.String({
|
|
211
|
+
description:
|
|
212
|
+
'Start time — relative (15m, 1h, 7d) or ISO 8601. Defaults to project config or 1h.',
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
to: Type.Optional(
|
|
216
|
+
Type.String({
|
|
217
|
+
description: 'End time — relative, ISO 8601, or "now". Defaults to "now".',
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
limit: Type.Optional(
|
|
221
|
+
Type.Number({
|
|
222
|
+
description: 'Max events to return (1-100). Default 25.',
|
|
223
|
+
minimum: 1,
|
|
224
|
+
maximum: 100,
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
sort: Type.Optional(
|
|
228
|
+
StringEnum(SORT_ENUM, {
|
|
229
|
+
description: 'Sort order by timestamp. Default "newest".',
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
service: Type.Optional(
|
|
233
|
+
Type.String({
|
|
234
|
+
description:
|
|
235
|
+
'Service name — overrides project rumService/service default from .pi/datadog.json.',
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
env: Type.Optional(
|
|
239
|
+
Type.String({
|
|
240
|
+
description: 'Environment — overrides project default from .pi/datadog.json.',
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
applicationId: Type.Optional(
|
|
244
|
+
Type.String({
|
|
245
|
+
description:
|
|
246
|
+
'RUM application id (@application.id) — overrides project rumApplicationId default.',
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
}),
|
|
250
|
+
|
|
251
|
+
async execute(
|
|
252
|
+
_toolCallId: string,
|
|
253
|
+
params: {
|
|
254
|
+
query: string;
|
|
255
|
+
from?: string;
|
|
256
|
+
to?: string;
|
|
257
|
+
limit?: number;
|
|
258
|
+
sort?: (typeof SORT_ENUM)[number];
|
|
259
|
+
service?: string;
|
|
260
|
+
env?: string;
|
|
261
|
+
applicationId?: string;
|
|
262
|
+
},
|
|
263
|
+
_signal?: AbortSignal,
|
|
264
|
+
_onUpdate?: unknown,
|
|
265
|
+
ctx?: { cwd: string },
|
|
266
|
+
) {
|
|
267
|
+
// Ensure .env is loaded in case the tool runs before session_start.
|
|
268
|
+
if (ctx?.cwd) loadDotEnv(ctx.cwd);
|
|
269
|
+
|
|
270
|
+
const credentials = getCredentials();
|
|
271
|
+
if (!credentials) {
|
|
272
|
+
const status = getCredentialStatus();
|
|
273
|
+
const missing = [
|
|
274
|
+
!status.hasApiKey && 'DD_API_KEY',
|
|
275
|
+
!status.hasAppKey && 'DD_APP_KEY',
|
|
276
|
+
].filter(Boolean);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text' as const,
|
|
282
|
+
text: `❌ Missing Datadog credentials: ${missing.join(', ')}.\n\nSet these environment variables to enable Datadog RUM search.`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
details: { error: 'missing_credentials', missing } as Record<string, unknown>,
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Reload config if not loaded yet (e.g. tool called before session_start)
|
|
291
|
+
if (!projectConfig && ctx?.cwd) {
|
|
292
|
+
try {
|
|
293
|
+
projectConfig = await loadProjectConfig(ctx.cwd);
|
|
294
|
+
} catch {
|
|
295
|
+
projectConfig = { site: 'datadoghq.com', defaultTimeRange: '1h' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const config = projectConfig ?? { site: 'datadoghq.com', defaultTimeRange: '1h' };
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const result = await searchRumEvents(params, config, credentials);
|
|
303
|
+
const summary = formatRumSearchSummary(result);
|
|
304
|
+
|
|
305
|
+
// Write the full, untruncated results to a temp file the agent can read,
|
|
306
|
+
// and return only a compact digest inline to save tokens.
|
|
307
|
+
let resultsFile: string | undefined;
|
|
308
|
+
if (result.events.length > 0) {
|
|
309
|
+
try {
|
|
310
|
+
const fullContent = formatRumSearchResult(result, {
|
|
311
|
+
maxAttributesLength: Infinity,
|
|
312
|
+
});
|
|
313
|
+
resultsFile = await writeResultsFile(fullContent, 'rum');
|
|
314
|
+
} catch {
|
|
315
|
+
// Non-fatal: digest still carries the summary.
|
|
316
|
+
resultsFile = undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: 'text' as const, text: formatRumResultDigest(result, resultsFile) }],
|
|
322
|
+
details: { ...summary, resultsFile } as Record<string, unknown>,
|
|
323
|
+
};
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const info = describeDatadogError(err);
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: 'text' as const,
|
|
330
|
+
text: `❌ ${info.message}`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
details: {
|
|
334
|
+
error: info.isRateLimit ? 'rate_limited' : 'api_error',
|
|
335
|
+
code: info.code,
|
|
336
|
+
message: info.message,
|
|
337
|
+
} as Record<string, unknown>,
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
182
344
|
pi.registerCommand('datadog', {
|
|
183
345
|
description: 'Show Datadog configuration and connection status',
|
|
184
346
|
handler: async (
|
|
@@ -210,6 +372,8 @@ export default function datadogExtension(pi: ExtensionAPI) {
|
|
|
210
372
|
if (config.defaultTags?.length) {
|
|
211
373
|
lines.push(` Default tags: ${config.defaultTags.join(', ')}`);
|
|
212
374
|
}
|
|
375
|
+
lines.push(` RUM application: ${config.rumApplicationId ?? '(not set)'}`);
|
|
376
|
+
lines.push(` RUM service: ${config.rumService ?? '(not set)'}`);
|
|
213
377
|
} else {
|
|
214
378
|
lines.push('No .pi/datadog.json found — using defaults.');
|
|
215
379
|
}
|
|
@@ -7,11 +7,14 @@ import { join } from 'node:path';
|
|
|
7
7
|
* read freely with the `read` tool, sidestepping the inline truncation that
|
|
8
8
|
* hides long log messages and attributes.
|
|
9
9
|
*
|
|
10
|
+
* The `prefix` controls the filename stem (e.g. "logs" → `logs-<ts>.md`,
|
|
11
|
+
* "rum" → `rum-<ts>.md`) so logs and RUM results stay distinguishable on disk.
|
|
12
|
+
*
|
|
10
13
|
* Returns the absolute path to the written file.
|
|
11
14
|
*/
|
|
12
|
-
export async function writeResultsFile(content: string): Promise<string> {
|
|
15
|
+
export async function writeResultsFile(content: string, prefix = 'logs'): Promise<string> {
|
|
13
16
|
const dir = await mkdtemp(join(tmpdir(), 'pi-datadog-'));
|
|
14
|
-
const path = join(dir,
|
|
17
|
+
const path = join(dir, `${prefix}-${Date.now()}.md`);
|
|
15
18
|
await writeFile(path, content, 'utf-8');
|
|
16
19
|
return path;
|
|
17
20
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { v2 } from '@datadog/datadog-api-client';
|
|
2
|
+
import type { DatadogCredentials, DatadogProjectConfig } from './config.js';
|
|
3
|
+
import { createConfiguration, resolveTime } from './client.js';
|
|
4
|
+
|
|
5
|
+
export interface RumSearchParams {
|
|
6
|
+
query: string;
|
|
7
|
+
from?: string;
|
|
8
|
+
to?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
sort?: 'newest' | 'oldest';
|
|
11
|
+
service?: string;
|
|
12
|
+
env?: string;
|
|
13
|
+
applicationId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RumEvent {
|
|
17
|
+
id: string;
|
|
18
|
+
/** RUM event type (session, view, action, error, …) from the event attributes. */
|
|
19
|
+
eventType: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
service: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
attributes: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RumSearchResult {
|
|
27
|
+
events: RumEvent[];
|
|
28
|
+
totalCount: number;
|
|
29
|
+
query: string;
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
cursor?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the full RUM query by merging the user query with config defaults.
|
|
37
|
+
*
|
|
38
|
+
* Defaults to session events (`@type:session`) unless the query already scopes
|
|
39
|
+
* `@type`, so views/actions/errors stay reachable. Application, service, env,
|
|
40
|
+
* and tag defaults are merged the same way as the logs client, skipping any
|
|
41
|
+
* dimension the user already constrained.
|
|
42
|
+
*/
|
|
43
|
+
export function buildRumQuery(params: RumSearchParams, config: DatadogProjectConfig): string {
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
if (params.query.trim().length > 0) parts.push(params.query);
|
|
46
|
+
|
|
47
|
+
if (!params.query.includes('@type:')) {
|
|
48
|
+
parts.push('@type:session');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const applicationId = params.applicationId ?? config.rumApplicationId;
|
|
52
|
+
if (applicationId && !params.query.includes('@application.id:')) {
|
|
53
|
+
parts.push(`@application.id:${applicationId}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const service = params.service ?? config.rumService ?? config.service;
|
|
57
|
+
if (service && !params.query.includes('service:')) {
|
|
58
|
+
parts.push(`service:${service}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const env = params.env ?? config.env;
|
|
62
|
+
if (env && !params.query.includes('env:')) {
|
|
63
|
+
parts.push(`env:${env}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.defaultTags) {
|
|
67
|
+
for (const tag of config.defaultTags) {
|
|
68
|
+
const tagKey = tag.split(':')[0];
|
|
69
|
+
if (!params.query.includes(`${tagKey}:`)) {
|
|
70
|
+
parts.push(tag);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return parts.join(' ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalizes a raw RUM event from the SDK into a flat, agent-friendly shape.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeRumEvent(event: v2.RUMEvent): RumEvent {
|
|
82
|
+
const attrs = event.attributes;
|
|
83
|
+
const nested = (attrs?.attributes as Record<string, unknown> | undefined) ?? {};
|
|
84
|
+
const eventType = typeof nested.type === 'string' ? nested.type : 'unknown';
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
id: event.id ?? 'unknown',
|
|
88
|
+
eventType,
|
|
89
|
+
timestamp: attrs?.timestamp?.toISOString() ?? 'unknown',
|
|
90
|
+
service: attrs?.service ?? 'unknown',
|
|
91
|
+
tags: attrs?.tags ?? [],
|
|
92
|
+
attributes: nested,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Searches Datadog RUM events using the v2 API.
|
|
98
|
+
*/
|
|
99
|
+
export async function searchRumEvents(
|
|
100
|
+
params: RumSearchParams,
|
|
101
|
+
config: DatadogProjectConfig,
|
|
102
|
+
credentials: DatadogCredentials,
|
|
103
|
+
): Promise<RumSearchResult> {
|
|
104
|
+
const fullQuery = buildRumQuery(params, config);
|
|
105
|
+
const fromTime = resolveTime(params.from ?? config.defaultTimeRange);
|
|
106
|
+
const toTime = resolveTime(params.to ?? 'now');
|
|
107
|
+
const limit = Math.min(params.limit ?? 25, 100);
|
|
108
|
+
const sortOrder = params.sort === 'oldest' ? 'timestamp' : '-timestamp';
|
|
109
|
+
|
|
110
|
+
const api = new v2.RUMApi(createConfiguration(credentials, config.site));
|
|
111
|
+
|
|
112
|
+
const response = await api.listRUMEvents({
|
|
113
|
+
filterQuery: fullQuery,
|
|
114
|
+
filterFrom: fromTime,
|
|
115
|
+
filterTo: toTime,
|
|
116
|
+
pageLimit: limit,
|
|
117
|
+
sort: sortOrder as v2.RUMSort,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const events = (response.data ?? []).map(normalizeRumEvent);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
events,
|
|
124
|
+
totalCount: events.length,
|
|
125
|
+
query: fullQuery,
|
|
126
|
+
from: fromTime.toISOString(),
|
|
127
|
+
to: toTime.toISOString(),
|
|
128
|
+
cursor: response.meta?.page?.after,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { RumEvent, RumSearchResult } from './rum-client.js';
|
|
2
|
+
|
|
3
|
+
const MAX_ATTRIBUTES_LENGTH = 300;
|
|
4
|
+
|
|
5
|
+
export interface RumFormatOptions {
|
|
6
|
+
/** Max attributes JSON length before truncation. Use Infinity to disable. */
|
|
7
|
+
maxAttributesLength?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Truncates a string to the given max length, appending "…" if truncated.
|
|
12
|
+
* A maxLength of Infinity disables truncation.
|
|
13
|
+
*/
|
|
14
|
+
function truncate(text: string, maxLength: number): string {
|
|
15
|
+
if (text.length <= maxLength) return text;
|
|
16
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTypeIcon(eventType: string): string {
|
|
20
|
+
switch (eventType.toLowerCase()) {
|
|
21
|
+
case 'error':
|
|
22
|
+
return '🔴';
|
|
23
|
+
case 'session':
|
|
24
|
+
return '👤';
|
|
25
|
+
case 'view':
|
|
26
|
+
return '📄';
|
|
27
|
+
case 'action':
|
|
28
|
+
return '🖱️';
|
|
29
|
+
case 'long_task':
|
|
30
|
+
case 'resource':
|
|
31
|
+
return '📦';
|
|
32
|
+
default:
|
|
33
|
+
return '⚪';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Reads a dotted-ish nested value (e.g. session.id) from the attribute bag. */
|
|
38
|
+
function readNested(
|
|
39
|
+
attributes: Record<string, unknown>,
|
|
40
|
+
group: string,
|
|
41
|
+
key: string,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
const obj = attributes[group];
|
|
44
|
+
if (typeof obj === 'object' && obj !== null && key in obj) {
|
|
45
|
+
const value = (obj as Record<string, unknown>)[key];
|
|
46
|
+
if (value !== undefined && value !== null) return String(value);
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatRumEvent(event: RumEvent, index: number, opts: Required<RumFormatOptions>): string {
|
|
52
|
+
const icon = getTypeIcon(event.eventType);
|
|
53
|
+
const header = `### ${index + 1}. ${icon} \`${event.eventType}\` — ${event.timestamp}`;
|
|
54
|
+
|
|
55
|
+
const lines: string[] = [header];
|
|
56
|
+
|
|
57
|
+
if (event.service !== 'unknown') lines.push(`**Service:** ${event.service}`);
|
|
58
|
+
|
|
59
|
+
const sessionId = readNested(event.attributes, 'session', 'id');
|
|
60
|
+
if (sessionId) lines.push(`**Session:** ${sessionId}`);
|
|
61
|
+
|
|
62
|
+
const viewUrl =
|
|
63
|
+
readNested(event.attributes, 'view', 'url') ?? readNested(event.attributes, 'view', 'url_path');
|
|
64
|
+
if (viewUrl) lines.push(`**View:** ${viewUrl}`);
|
|
65
|
+
|
|
66
|
+
const attrKeys = Object.keys(event.attributes);
|
|
67
|
+
if (attrKeys.length > 0) {
|
|
68
|
+
const attrStr = truncate(JSON.stringify(event.attributes, null, 2), opts.maxAttributesLength);
|
|
69
|
+
lines.push(`**Attributes:**\n\`\`\`json\n${attrStr}\n\`\`\``);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event.tags.length > 0) {
|
|
73
|
+
lines.push(`**Tags:** ${event.tags.map((t) => `\`${t}\``).join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Formats the RUM search result for LLM consumption.
|
|
81
|
+
*
|
|
82
|
+
* By default attributes are truncated for a concise inline view. Pass
|
|
83
|
+
* `{ maxAttributesLength: Infinity }` to render the complete, untruncated
|
|
84
|
+
* result (e.g. for writing to a temp file).
|
|
85
|
+
*/
|
|
86
|
+
export function formatRumSearchResult(
|
|
87
|
+
result: RumSearchResult,
|
|
88
|
+
opts: RumFormatOptions = {},
|
|
89
|
+
): string {
|
|
90
|
+
const resolved: Required<RumFormatOptions> = {
|
|
91
|
+
maxAttributesLength: opts.maxAttributesLength ?? MAX_ATTRIBUTES_LENGTH,
|
|
92
|
+
};
|
|
93
|
+
const lines: string[] = [];
|
|
94
|
+
|
|
95
|
+
lines.push(`## Datadog RUM Event Search Results`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push(`**Query:** \`${result.query}\``);
|
|
98
|
+
lines.push(`**Time range:** ${result.from} → ${result.to}`);
|
|
99
|
+
lines.push(`**Results:** ${result.totalCount} events returned`);
|
|
100
|
+
|
|
101
|
+
if (result.cursor) {
|
|
102
|
+
lines.push(`**Pagination:** More results available (cursor present)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('');
|
|
106
|
+
|
|
107
|
+
if (result.events.length === 0) {
|
|
108
|
+
lines.push('No RUM events found matching the query.');
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push('---');
|
|
113
|
+
lines.push('');
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < result.events.length; i++) {
|
|
116
|
+
lines.push(formatRumEvent(result.events[i], i, resolved));
|
|
117
|
+
if (i < result.events.length - 1) lines.push('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds a compact, token-light digest of a RUM search result for inline tool
|
|
125
|
+
* output. The full per-entry detail lives in the temp file referenced by
|
|
126
|
+
* `resultsFile`; this just gives the agent enough to decide whether to read it.
|
|
127
|
+
*/
|
|
128
|
+
export function formatRumResultDigest(result: RumSearchResult, resultsFile?: string): string {
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
lines.push(
|
|
131
|
+
`## Datadog RUM Search — ${result.totalCount} RUM event${result.totalCount === 1 ? '' : 's'}`,
|
|
132
|
+
);
|
|
133
|
+
lines.push(`**Query:** \`${result.query}\``);
|
|
134
|
+
lines.push(`**Time range:** ${result.from} → ${result.to}`);
|
|
135
|
+
|
|
136
|
+
if (result.events.length === 0) {
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('No RUM events found matching the query.');
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const summary = formatRumSearchSummary(result);
|
|
143
|
+
const breakdown = Object.entries(summary.typeBreakdown)
|
|
144
|
+
.map(([type, count]) => `${type}: ${count}`)
|
|
145
|
+
.join(', ');
|
|
146
|
+
if (breakdown) lines.push(`**Types:** ${breakdown}`);
|
|
147
|
+
if (summary.services.length > 0) lines.push(`**Services:** ${summary.services.join(', ')}`);
|
|
148
|
+
if (result.cursor) lines.push(`**Pagination:** more results available (cursor present)`);
|
|
149
|
+
|
|
150
|
+
if (resultsFile) {
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(
|
|
153
|
+
`📄 **Full results** (complete event attributes): \`${resultsFile}\`\nUse the \`read\` tool on this file to view every RUM event.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Formats a summary of the RUM search result for tool details.
|
|
162
|
+
*/
|
|
163
|
+
export function formatRumSearchSummary(result: RumSearchResult): {
|
|
164
|
+
totalCount: number;
|
|
165
|
+
query: string;
|
|
166
|
+
timeRange: { from: string; to: string };
|
|
167
|
+
typeBreakdown: Record<string, number>;
|
|
168
|
+
services: string[];
|
|
169
|
+
hasCursor: boolean;
|
|
170
|
+
} {
|
|
171
|
+
const typeBreakdown: Record<string, number> = {};
|
|
172
|
+
const services = new Set<string>();
|
|
173
|
+
|
|
174
|
+
for (const event of result.events) {
|
|
175
|
+
const type = event.eventType.toLowerCase();
|
|
176
|
+
typeBreakdown[type] = (typeBreakdown[type] ?? 0) + 1;
|
|
177
|
+
if (event.service !== 'unknown') services.add(event.service);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
totalCount: result.totalCount,
|
|
182
|
+
query: result.query,
|
|
183
|
+
timeRange: { from: result.from, to: result.to },
|
|
184
|
+
typeBreakdown,
|
|
185
|
+
services: [...services],
|
|
186
|
+
hasCursor: Boolean(result.cursor),
|
|
187
|
+
};
|
|
188
|
+
}
|