@dreki-gg/pi-datadog 0.2.4 → 0.4.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @dreki-gg/pi-datadog
2
2
 
3
+ ## [0.4.0](https://github.com/jalbarrang/pi-datadog/compare/v0.3.0...v0.4.0) (2026-07-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * extract pi-datadog from dreki-gg/pi-extensions monorepo ([9ee1fb0](https://github.com/jalbarrang/pi-datadog/commit/9ee1fb0c4989f3a271c077ba134c88c6bf9808ec))
9
+
10
+ ## 0.3.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 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.
15
+
3
16
  ## 0.2.4
4
17
 
5
18
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Albarran
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 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
- function createApiInstance(credentials: DatadogCredentials, site: string): v2.LogsApi {
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
- return new v2.LogsApi(configuration);
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, `logs-${Date.now()}.md`);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-datadog",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Datadog log search tools for pi — query production logs with project-aware context",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -13,8 +13,7 @@
13
13
  "license": "MIT",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/dreki-gg/pi-extensions",
17
- "directory": "packages/datadog"
16
+ "url": "git+https://github.com/jalbarrang/pi-datadog.git"
18
17
  },
19
18
  "type": "module",
20
19
  "files": [
@@ -43,7 +42,9 @@
43
42
  "bun-types": "^1.3.14",
44
43
  "oxfmt": "^0.43.0",
45
44
  "oxlint": "^1.58.0",
46
- "typescript": "^6.0.0"
45
+ "typescript": "^6.0.0",
46
+ "@earendil-works/pi-coding-agent": "^0.75.4",
47
+ "typebox": "^1.0.55"
47
48
  },
48
49
  "peerDependencies": {
49
50
  "@earendil-works/pi-coding-agent": "*",