@rawdash/connector-new-relic 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ <!-- This file is generated from connector metadata by scripts/generate-connector-docs.ts. Do not edit by hand. -->
2
+
3
+ # @rawdash/connector-new-relic
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@rawdash/connector-new-relic)](https://www.npmjs.com/package/@rawdash/connector-new-relic)
6
+ [![license](https://img.shields.io/npm/l/@rawdash/connector-new-relic)](https://github.com/rawdash/rawdash/blob/main/LICENSE)
7
+
8
+ Sync NRQL alert conditions, AI incidents, and user-declared NRQL metric queries from a New Relic account via NerdGraph.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @rawdash/connector-new-relic
14
+ ```
15
+
16
+ ## Authentication
17
+
18
+ A New Relic User API key plus the numeric account ID are required. The key is stored as a secret and used to authenticate every NerdGraph GraphQL request.
19
+
20
+ 1. Open New Relic -> API keys and create a `User` key. Ingest-keys are not accepted by NerdGraph.
21
+ 2. Find the numeric account ID under New Relic -> Administration -> Access management. The User key must have access to that account.
22
+ 3. Store the User key as a secret and reference it from the connector config as `apiKey: secret("NEWRELIC_USER_KEY")`.
23
+ 4. Set `accountId` to the numeric account ID, and optionally `region: "EU"` if the data lives on the EU host (`api.eu.newrelic.com`); defaults to `US` (`api.newrelic.com`).
24
+
25
+ ## Configuration
26
+
27
+ | Field | Type | Required | Description |
28
+ | ------------------------ | ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `apiKey` | secret | Yes | New Relic User API key. Create at New Relic -> API keys (User key type, ingest-keys do not work for NerdGraph). |
30
+ | `accountId` | number | Yes | New Relic account ID the User API key has access to. Find it under New Relic -> Administration -> Access management. |
31
+ | `region` | `US` \| `EU` | No | New Relic data region. Defaults to `US` (`api.newrelic.com`); set to `EU` to use `api.eu.newrelic.com`. |
32
+ | `nrqlQueries` | array | No | User-declared NRQL queries. Each entry produces `newrelic_nrql_metric` samples named `<name>` from the NerdGraph NRQL API. |
33
+ | `resources` | array | No | Which New Relic resources to sync. Omit to sync all of them. |
34
+ | `incidentsLookbackHours` | number | No | Window of NrAiIncident rows to pull on each sync, in hours. Defaults to 168 (7 days). Ignored when `since` is set by the host. |
35
+ | `metricsLookbackHours` | number | No | Window of NRQL metric samples to pull on each sync, in hours. Defaults to 24. Each user query gets `SINCE <lookback> hours ago` appended unless the query already declares its own `SINCE` clause. |
36
+
37
+ ## Resources
38
+
39
+ - **`newrelic_alert_condition`** _(entity)_ - NRQL alert conditions with name, enabled state, policy id, type, and the underlying NRQL query string.
40
+ - Endpoint: `GraphQL query: actor.account.alerts.nrqlConditionsSearch { nrqlConditions { ... } }`
41
+ - **`newrelic_alert_violation`** _(event)_ - AI alert violation events. Each row from the NrAiIncident event type becomes one event with openedAt / closedAt and the underlying condition / policy metadata.
42
+ - Endpoint: `GraphQL nrql() against `SELECT ... FROM NrAiIncident WHERE openedAt > ...``
43
+ - Append-only across syncs; the connector filters NrAiIncident by `openedAt` against `options.since` (or the configured lookback) to avoid re-emitting old incidents.
44
+ - **`newrelic_nrql_metric`** _(metric)_ - User-declared NRQL metric samples, stored as `newrelic_nrql_metric.<query name>`. Each NRQL result row is mapped to a single sample using the first numeric, non-timestamp/facet field as the value.
45
+ - Endpoint: `GraphQL nrql() against the user-declared NRQL query`
46
+ - Dimensions: `queryName`, `query`, `facets`
47
+
48
+ ## Example
49
+
50
+ ```ts
51
+ import {
52
+ defineConfig,
53
+ defineDashboard,
54
+ defineMetric,
55
+ secret,
56
+ } from '@rawdash/core';
57
+
58
+ const newRelic = {
59
+ name: 'newrelic',
60
+ connectorId: 'new-relic',
61
+ config: {
62
+ apiKey: secret('NEWRELIC_USER_KEY'),
63
+ accountId: 1234567,
64
+ region: 'US' as const,
65
+ nrqlQueries: [
66
+ {
67
+ name: 'error_rate',
68
+ query:
69
+ 'SELECT percentage(count(*), WHERE error IS true) FROM Transaction TIMESERIES 5 minutes',
70
+ },
71
+ ],
72
+ },
73
+ };
74
+
75
+ export default defineConfig({
76
+ connectors: [newRelic],
77
+ dashboards: {
78
+ observability: defineDashboard({
79
+ widgets: {
80
+ open_violations: {
81
+ kind: 'stat',
82
+ title: 'Open Alert Violations',
83
+ metric: defineMetric({
84
+ connector: newRelic,
85
+ shape: 'event',
86
+ name: 'newrelic_alert_violation',
87
+ fn: 'count',
88
+ filter: [{ field: 'state', op: 'eq', value: 'CREATED' }],
89
+ }),
90
+ },
91
+ },
92
+ }),
93
+ },
94
+ });
95
+ ```
96
+
97
+ ## Rate limits
98
+
99
+ NerdGraph enforces per-account NRQL quotas; this connector retries on 429s through the standard HTTP retry policy.
100
+
101
+ ## Limitations
102
+
103
+ - APM-trace deep inspection is out of scope (not dashboard-shaped).
104
+ - NRQL queries are single-shot per sync (NRQL does not support cursor pagination); large queries should narrow their `SINCE` window or use `LIMIT MAX`.
105
+ - Incidents are pulled via NRQL on the NrAiIncident event type, which depends on Applied Intelligence being enabled on the account.
106
+ - Only NRQL-based alert conditions are synced; legacy V1 condition types are not exposed.
107
+
108
+ ## Links
109
+
110
+ - [Rawdash docs](https://rawdash.dev/docs/connectors/)
111
+ - [New Relic API docs](https://docs.newrelic.com/docs/apis/nerdgraph/)
112
+ - [GitHub](https://github.com/rawdash/rawdash)
113
+
114
+ ## License
115
+
116
+ Apache-2.0
@@ -0,0 +1,257 @@
1
+ import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, ConnectorDoc } from '@rawdash/core';
2
+ import { z } from 'zod';
3
+
4
+ declare const configFields: z.ZodObject<{
5
+ apiKey: z.ZodObject<{
6
+ $secret: z.ZodString;
7
+ }, z.core.$strip>;
8
+ accountId: z.ZodNumber;
9
+ region: z.ZodOptional<z.ZodEnum<{
10
+ US: "US";
11
+ EU: "EU";
12
+ }>>;
13
+ nrqlQueries: z.ZodOptional<z.ZodArray<z.ZodObject<{
14
+ name: z.ZodString;
15
+ query: z.ZodString;
16
+ }, z.core.$strip>>>;
17
+ resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
18
+ alerts: "alerts";
19
+ alert_violations: "alert_violations";
20
+ nrql_queries: "nrql_queries";
21
+ }>>>;
22
+ incidentsLookbackHours: z.ZodOptional<z.ZodNumber>;
23
+ metricsLookbackHours: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strip>;
25
+ declare const doc: ConnectorDoc;
26
+ type NewRelicResource = 'alerts' | 'alert_violations' | 'nrql_queries';
27
+ interface NewRelicNrqlQuery {
28
+ name: string;
29
+ query: string;
30
+ }
31
+ interface NewRelicSettings {
32
+ accountId: number;
33
+ region?: 'US' | 'EU';
34
+ nrqlQueries?: readonly NewRelicNrqlQuery[];
35
+ resources?: readonly NewRelicResource[];
36
+ incidentsLookbackHours?: number;
37
+ metricsLookbackHours?: number;
38
+ }
39
+ declare const newRelicCredentials: {
40
+ apiKey: {
41
+ description: string;
42
+ auth: "required";
43
+ };
44
+ };
45
+ type NewRelicCredentials = typeof newRelicCredentials;
46
+ declare const newRelicResources: {
47
+ readonly newrelic_alert_condition: {
48
+ readonly shape: "entity";
49
+ readonly description: "NRQL alert conditions with name, enabled state, policy id, type, and the underlying NRQL query string.";
50
+ readonly endpoint: "GraphQL query: actor.account.alerts.nrqlConditionsSearch { nrqlConditions { ... } }";
51
+ readonly responses: {
52
+ readonly alert_conditions: z.ZodObject<{
53
+ nrqlConditions: z.ZodArray<z.ZodObject<{
54
+ id: z.ZodString;
55
+ name: z.ZodString;
56
+ enabled: z.ZodBoolean;
57
+ policyId: z.ZodString;
58
+ type: z.ZodString;
59
+ createdAt: z.ZodNullable<z.ZodNumber>;
60
+ updatedAt: z.ZodNullable<z.ZodNumber>;
61
+ nrql: z.ZodOptional<z.ZodNullable<z.ZodObject<{
62
+ query: z.ZodString;
63
+ }, z.core.$strip>>>;
64
+ }, z.core.$strip>>;
65
+ nextCursor: z.ZodNullable<z.ZodString>;
66
+ totalCount: z.ZodNumber;
67
+ }, z.core.$strip>;
68
+ };
69
+ };
70
+ readonly newrelic_alert_violation: {
71
+ readonly shape: "event";
72
+ readonly description: "AI alert violation events. Each row from the NrAiIncident event type becomes one event with openedAt / closedAt and the underlying condition / policy metadata.";
73
+ readonly endpoint: "GraphQL nrql() against `SELECT ... FROM NrAiIncident WHERE openedAt > ...`";
74
+ readonly notes: "Append-only across syncs; the connector filters NrAiIncident by `openedAt` against `options.since` (or the configured lookback) to avoid re-emitting old incidents.";
75
+ readonly responses: {
76
+ readonly incidents: z.ZodObject<{
77
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
78
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
79
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
80
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
81
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
82
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
83
+ }, z.core.$strip>>>;
84
+ }, z.core.$strip>>>;
85
+ }, z.core.$strip>;
86
+ };
87
+ };
88
+ readonly newrelic_nrql_metric: {
89
+ readonly shape: "metric";
90
+ readonly dynamic: true;
91
+ readonly description: "User-declared NRQL metric samples, stored as `newrelic_nrql_metric.<query name>`. Each NRQL result row is mapped to a single sample using the first numeric, non-timestamp/facet field as the value.";
92
+ readonly endpoint: "GraphQL nrql() against the user-declared NRQL query";
93
+ readonly dimensions: [{
94
+ readonly name: "queryName";
95
+ readonly description: "The user-declared query name.";
96
+ }, {
97
+ readonly name: "query";
98
+ readonly description: "The NRQL query string.";
99
+ }, {
100
+ readonly name: "facets";
101
+ readonly description: "Comma-joined facet values for the row, or `*` when the result row is ungrouped.";
102
+ }];
103
+ readonly responses: {
104
+ readonly nrql_queries: z.ZodObject<{
105
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
106
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
107
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
108
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
109
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
110
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
111
+ }, z.core.$strip>>>;
112
+ }, z.core.$strip>>>;
113
+ }, z.core.$strip>;
114
+ };
115
+ };
116
+ };
117
+ declare const id = "new-relic";
118
+ declare class NewRelicConnector extends BaseConnector<NewRelicSettings, NewRelicCredentials> {
119
+ static readonly id = "new-relic";
120
+ static readonly resources: {
121
+ readonly newrelic_alert_condition: {
122
+ readonly shape: "entity";
123
+ readonly description: "NRQL alert conditions with name, enabled state, policy id, type, and the underlying NRQL query string.";
124
+ readonly endpoint: "GraphQL query: actor.account.alerts.nrqlConditionsSearch { nrqlConditions { ... } }";
125
+ readonly responses: {
126
+ readonly alert_conditions: z.ZodObject<{
127
+ nrqlConditions: z.ZodArray<z.ZodObject<{
128
+ id: z.ZodString;
129
+ name: z.ZodString;
130
+ enabled: z.ZodBoolean;
131
+ policyId: z.ZodString;
132
+ type: z.ZodString;
133
+ createdAt: z.ZodNullable<z.ZodNumber>;
134
+ updatedAt: z.ZodNullable<z.ZodNumber>;
135
+ nrql: z.ZodOptional<z.ZodNullable<z.ZodObject<{
136
+ query: z.ZodString;
137
+ }, z.core.$strip>>>;
138
+ }, z.core.$strip>>;
139
+ nextCursor: z.ZodNullable<z.ZodString>;
140
+ totalCount: z.ZodNumber;
141
+ }, z.core.$strip>;
142
+ };
143
+ };
144
+ readonly newrelic_alert_violation: {
145
+ readonly shape: "event";
146
+ readonly description: "AI alert violation events. Each row from the NrAiIncident event type becomes one event with openedAt / closedAt and the underlying condition / policy metadata.";
147
+ readonly endpoint: "GraphQL nrql() against `SELECT ... FROM NrAiIncident WHERE openedAt > ...`";
148
+ readonly notes: "Append-only across syncs; the connector filters NrAiIncident by `openedAt` against `options.since` (or the configured lookback) to avoid re-emitting old incidents.";
149
+ readonly responses: {
150
+ readonly incidents: z.ZodObject<{
151
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
152
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
153
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
154
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
155
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
156
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
157
+ }, z.core.$strip>>>;
158
+ }, z.core.$strip>>>;
159
+ }, z.core.$strip>;
160
+ };
161
+ };
162
+ readonly newrelic_nrql_metric: {
163
+ readonly shape: "metric";
164
+ readonly dynamic: true;
165
+ readonly description: "User-declared NRQL metric samples, stored as `newrelic_nrql_metric.<query name>`. Each NRQL result row is mapped to a single sample using the first numeric, non-timestamp/facet field as the value.";
166
+ readonly endpoint: "GraphQL nrql() against the user-declared NRQL query";
167
+ readonly dimensions: [{
168
+ readonly name: "queryName";
169
+ readonly description: "The user-declared query name.";
170
+ }, {
171
+ readonly name: "query";
172
+ readonly description: "The NRQL query string.";
173
+ }, {
174
+ readonly name: "facets";
175
+ readonly description: "Comma-joined facet values for the row, or `*` when the result row is ungrouped.";
176
+ }];
177
+ readonly responses: {
178
+ readonly nrql_queries: z.ZodObject<{
179
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
180
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
181
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
182
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
183
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
184
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
185
+ }, z.core.$strip>>>;
186
+ }, z.core.$strip>>>;
187
+ }, z.core.$strip>;
188
+ };
189
+ };
190
+ };
191
+ static readonly schemas: {
192
+ readonly alert_conditions: z.ZodObject<{
193
+ nrqlConditions: z.ZodArray<z.ZodObject<{
194
+ id: z.ZodString;
195
+ name: z.ZodString;
196
+ enabled: z.ZodBoolean;
197
+ policyId: z.ZodString;
198
+ type: z.ZodString;
199
+ createdAt: z.ZodNullable<z.ZodNumber>;
200
+ updatedAt: z.ZodNullable<z.ZodNumber>;
201
+ nrql: z.ZodOptional<z.ZodNullable<z.ZodObject<{
202
+ query: z.ZodString;
203
+ }, z.core.$strip>>>;
204
+ }, z.core.$strip>>;
205
+ nextCursor: z.ZodNullable<z.ZodString>;
206
+ totalCount: z.ZodNumber;
207
+ }, z.core.$strip>;
208
+ } & {
209
+ readonly incidents: z.ZodObject<{
210
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
211
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
212
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
213
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
214
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
215
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
216
+ }, z.core.$strip>>>;
217
+ }, z.core.$strip>>>;
218
+ }, z.core.$strip>;
219
+ } & {
220
+ readonly nrql_queries: z.ZodObject<{
221
+ results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>>;
222
+ metadata: z.ZodOptional<z.ZodNullable<z.ZodObject<{
223
+ facets: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
224
+ timeWindow: z.ZodOptional<z.ZodNullable<z.ZodObject<{
225
+ begin: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
226
+ end: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
227
+ }, z.core.$strip>>>;
228
+ }, z.core.$strip>>>;
229
+ }, z.core.$strip>;
230
+ } & Readonly<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
231
+ static create(input: unknown, ctx?: ConnectorContext): NewRelicConnector;
232
+ readonly id = "new-relic";
233
+ readonly credentials: {
234
+ apiKey: {
235
+ description: string;
236
+ auth: "required";
237
+ };
238
+ };
239
+ private get endpoint();
240
+ private buildHeaders;
241
+ private graphql;
242
+ private activePhases;
243
+ private fetchAlertConditionsPage;
244
+ private fetchIncidents;
245
+ private fetchMetrics;
246
+ private appendSinceIfMissing;
247
+ private writeAlertConditions;
248
+ private writeIncidents;
249
+ private coerceScalar;
250
+ private writeMetrics;
251
+ private extractTimestamp;
252
+ private extractValue;
253
+ private extractFacets;
254
+ sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
255
+ }
256
+
257
+ export { NewRelicConnector, type NewRelicResource, type NewRelicSettings, configFields, NewRelicConnector as default, doc, id, newRelicResources as resources };
package/dist/index.js ADDED
@@ -0,0 +1,608 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/new-relic.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ defineConnectorDoc,
34
+ defineResources,
35
+ makeChunkedCursorGuard,
36
+ paginateChunked,
37
+ schemasFromResources,
38
+ selectActivePhases
39
+ } from "@rawdash/core";
40
+ import { z } from "zod";
41
+ var nrqlQuerySchema = z.object({
42
+ name: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/, {
43
+ message: "Metric name must be alphanumeric / underscore"
44
+ }),
45
+ query: z.string().min(1)
46
+ });
47
+ var configFields = defineConfigFields(
48
+ z.object({
49
+ apiKey: z.object({ $secret: z.string().min(1) }).meta({
50
+ label: "User API Key",
51
+ description: "New Relic User API key. Create at New Relic -> API keys (User key type, ingest-keys do not work for NerdGraph).",
52
+ placeholder: "NRAK-...",
53
+ secret: true
54
+ }),
55
+ accountId: z.number().int().positive().meta({
56
+ label: "Account ID",
57
+ description: "New Relic account ID the User API key has access to. Find it under New Relic -> Administration -> Access management.",
58
+ placeholder: "1234567"
59
+ }),
60
+ region: z.enum(["US", "EU"]).optional().meta({
61
+ label: "Region",
62
+ description: "New Relic data region. Defaults to `US` (`api.newrelic.com`); set to `EU` to use `api.eu.newrelic.com`.",
63
+ placeholder: "US"
64
+ }),
65
+ nrqlQueries: z.array(nrqlQuerySchema).nonempty().optional().meta({
66
+ label: "NRQL queries (optional)",
67
+ description: "User-declared NRQL queries. Each entry produces `newrelic_nrql_metric` samples named `<name>` from the NerdGraph NRQL API."
68
+ }),
69
+ resources: z.array(z.enum(["alerts", "alert_violations", "nrql_queries"])).nonempty().optional().meta({
70
+ label: "Resources",
71
+ description: "Which New Relic resources to sync. Omit to sync all of them."
72
+ }),
73
+ incidentsLookbackHours: z.number().int().positive().max(720).optional().meta({
74
+ label: "Incidents lookback (hours)",
75
+ description: "Window of NrAiIncident rows to pull on each sync, in hours. Defaults to 168 (7 days). Ignored when `since` is set by the host.",
76
+ placeholder: "168"
77
+ }),
78
+ metricsLookbackHours: z.number().int().positive().max(168).optional().meta({
79
+ label: "NRQL metrics lookback (hours)",
80
+ description: "Window of NRQL metric samples to pull on each sync, in hours. Defaults to 24. Each user query gets `SINCE <lookback> hours ago` appended unless the query already declares its own `SINCE` clause.",
81
+ placeholder: "24"
82
+ })
83
+ })
84
+ );
85
+ var doc = defineConnectorDoc({
86
+ displayName: "New Relic",
87
+ category: "infrastructure",
88
+ brandColor: "#1CE783",
89
+ tagline: "Sync NRQL alert conditions, AI incidents, and user-declared NRQL metric queries from a New Relic account via NerdGraph.",
90
+ vendor: {
91
+ name: "New Relic",
92
+ apiDocs: "https://docs.newrelic.com/docs/apis/nerdgraph/",
93
+ website: "https://newrelic.com"
94
+ },
95
+ auth: {
96
+ summary: "A New Relic User API key plus the numeric account ID are required. The key is stored as a secret and used to authenticate every NerdGraph GraphQL request.",
97
+ setup: [
98
+ "Open New Relic -> API keys and create a `User` key. Ingest-keys are not accepted by NerdGraph.",
99
+ "Find the numeric account ID under New Relic -> Administration -> Access management. The User key must have access to that account.",
100
+ 'Store the User key as a secret and reference it from the connector config as `apiKey: secret("NEWRELIC_USER_KEY")`.',
101
+ 'Set `accountId` to the numeric account ID, and optionally `region: "EU"` if the data lives on the EU host (`api.eu.newrelic.com`); defaults to `US` (`api.newrelic.com`).'
102
+ ]
103
+ },
104
+ rateLimit: "NerdGraph enforces per-account NRQL quotas; this connector retries on 429s through the standard HTTP retry policy.",
105
+ limitations: [
106
+ "APM-trace deep inspection is out of scope (not dashboard-shaped).",
107
+ "NRQL queries are single-shot per sync (NRQL does not support cursor pagination); large queries should narrow their `SINCE` window or use `LIMIT MAX`.",
108
+ "Incidents are pulled via NRQL on the NrAiIncident event type, which depends on Applied Intelligence being enabled on the account.",
109
+ "Only NRQL-based alert conditions are synced; legacy V1 condition types are not exposed."
110
+ ]
111
+ });
112
+ var newRelicCredentials = {
113
+ apiKey: {
114
+ description: "New Relic User API key",
115
+ auth: "required"
116
+ }
117
+ };
118
+ var PHASE_ORDER = ["alert_conditions", "incidents", "metrics"];
119
+ var isNewRelicSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
120
+ var ALERT_CONDITIONS_QUERY = `
121
+ query AlertConditions($accountId: Int!, $cursor: String) {
122
+ actor {
123
+ account(id: $accountId) {
124
+ alerts {
125
+ nrqlConditionsSearch(cursor: $cursor) {
126
+ nrqlConditions {
127
+ id
128
+ name
129
+ enabled
130
+ policyId
131
+ type
132
+ createdAt
133
+ updatedAt
134
+ nrql { query }
135
+ }
136
+ nextCursor
137
+ totalCount
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ `;
144
+ var NRQL_QUERY = `
145
+ query RunNrql($accountId: Int!, $query: Nrql!) {
146
+ actor {
147
+ account(id: $accountId) {
148
+ nrql(query: $query) {
149
+ results
150
+ metadata {
151
+ facets
152
+ timeWindow {
153
+ begin
154
+ end
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ `;
162
+ var idString = z.string().min(1);
163
+ var nrqlScalarSchema = z.union([
164
+ z.string(),
165
+ z.number(),
166
+ z.boolean(),
167
+ z.null()
168
+ ]);
169
+ var nrqlCellSchema = z.union([nrqlScalarSchema, z.array(nrqlScalarSchema)]);
170
+ var nrqlConditionNodeSchema = z.object({
171
+ id: idString,
172
+ name: z.string(),
173
+ enabled: z.boolean(),
174
+ policyId: idString,
175
+ type: z.string(),
176
+ createdAt: z.number().nullable(),
177
+ updatedAt: z.number().nullable(),
178
+ nrql: z.object({ query: z.string() }).nullable().optional()
179
+ });
180
+ var nrqlConditionsSearchResultSchema = z.object({
181
+ nrqlConditions: z.array(nrqlConditionNodeSchema),
182
+ nextCursor: z.string().nullable(),
183
+ totalCount: z.number().int().nonnegative()
184
+ });
185
+ var nrqlResultSchema = z.object({
186
+ results: z.array(z.record(z.string(), nrqlCellSchema)),
187
+ metadata: z.object({
188
+ facets: z.array(z.string()).nullable().optional(),
189
+ timeWindow: z.object({
190
+ begin: z.number().nullable().optional(),
191
+ end: z.number().nullable().optional()
192
+ }).nullable().optional()
193
+ }).nullable().optional()
194
+ });
195
+ var DEFAULT_REGION = "US";
196
+ var DEFAULT_INCIDENTS_LOOKBACK_HOURS = 168;
197
+ var DEFAULT_METRICS_LOOKBACK_HOURS = 24;
198
+ var INCIDENTS_NRQL_LIMIT = 5e3;
199
+ var TIMESTAMP_FIELDS = /* @__PURE__ */ new Set([
200
+ "timestamp",
201
+ "beginTimeSeconds",
202
+ "endTimeSeconds",
203
+ "begin_time",
204
+ "end_time",
205
+ "beginTime",
206
+ "endTime",
207
+ "openedAt",
208
+ "closedAt"
209
+ ]);
210
+ var FACET_FIELDS = /* @__PURE__ */ new Set(["facet"]);
211
+ var newRelicResources = defineResources({
212
+ newrelic_alert_condition: {
213
+ shape: "entity",
214
+ description: "NRQL alert conditions with name, enabled state, policy id, type, and the underlying NRQL query string.",
215
+ endpoint: "GraphQL query: actor.account.alerts.nrqlConditionsSearch { nrqlConditions { ... } }",
216
+ responses: { alert_conditions: nrqlConditionsSearchResultSchema }
217
+ },
218
+ newrelic_alert_violation: {
219
+ shape: "event",
220
+ description: "AI alert violation events. Each row from the NrAiIncident event type becomes one event with openedAt / closedAt and the underlying condition / policy metadata.",
221
+ endpoint: "GraphQL nrql() against `SELECT ... FROM NrAiIncident WHERE openedAt > ...`",
222
+ notes: "Append-only across syncs; the connector filters NrAiIncident by `openedAt` against `options.since` (or the configured lookback) to avoid re-emitting old incidents.",
223
+ responses: { incidents: nrqlResultSchema }
224
+ },
225
+ newrelic_nrql_metric: {
226
+ shape: "metric",
227
+ dynamic: true,
228
+ description: "User-declared NRQL metric samples, stored as `newrelic_nrql_metric.<query name>`. Each NRQL result row is mapped to a single sample using the first numeric, non-timestamp/facet field as the value.",
229
+ endpoint: "GraphQL nrql() against the user-declared NRQL query",
230
+ dimensions: [
231
+ { name: "queryName", description: "The user-declared query name." },
232
+ { name: "query", description: "The NRQL query string." },
233
+ {
234
+ name: "facets",
235
+ description: "Comma-joined facet values for the row, or `*` when the result row is ungrouped."
236
+ }
237
+ ],
238
+ responses: { nrql_queries: nrqlResultSchema }
239
+ }
240
+ });
241
+ var id = "new-relic";
242
+ var NewRelicConnector = class _NewRelicConnector extends BaseConnector {
243
+ static id = id;
244
+ static resources = newRelicResources;
245
+ static schemas = schemasFromResources(newRelicResources);
246
+ static create(input, ctx) {
247
+ const parsed = configFields.parse(input);
248
+ return new _NewRelicConnector(
249
+ {
250
+ accountId: parsed.accountId,
251
+ region: parsed.region,
252
+ nrqlQueries: parsed.nrqlQueries,
253
+ resources: parsed.resources,
254
+ incidentsLookbackHours: parsed.incidentsLookbackHours,
255
+ metricsLookbackHours: parsed.metricsLookbackHours
256
+ },
257
+ { apiKey: parsed.apiKey },
258
+ ctx
259
+ );
260
+ }
261
+ id = id;
262
+ credentials = newRelicCredentials;
263
+ get endpoint() {
264
+ const region = this.settings.region ?? DEFAULT_REGION;
265
+ return region === "EU" ? "https://api.eu.newrelic.com/graphql" : "https://api.newrelic.com/graphql";
266
+ }
267
+ buildHeaders() {
268
+ return {
269
+ "API-Key": this.creds.apiKey,
270
+ "Content-Type": "application/json",
271
+ "User-Agent": connectorUserAgent("new-relic")
272
+ };
273
+ }
274
+ async graphql(query, variables, resource, signal) {
275
+ const res = await this.post(this.endpoint, {
276
+ resource,
277
+ headers: this.buildHeaders(),
278
+ body: JSON.stringify({ query, variables }),
279
+ signal
280
+ });
281
+ if (res.body.errors && res.body.errors.length > 0) {
282
+ const messages = res.body.errors.map((e) => e.message).join("; ");
283
+ throw new Error(`New Relic NerdGraph error: ${messages}`);
284
+ }
285
+ if (!res.body.data) {
286
+ throw new Error(
287
+ `New Relic NerdGraph response missing data for resource '${resource}'`
288
+ );
289
+ }
290
+ return res;
291
+ }
292
+ // -------------------------------------------------------------------------
293
+ // Resource enablement
294
+ // -------------------------------------------------------------------------
295
+ activePhases() {
296
+ return selectActivePhases(
297
+ (r) => {
298
+ switch (r) {
299
+ case "alerts":
300
+ return "alert_conditions";
301
+ case "alert_violations":
302
+ return "incidents";
303
+ case "nrql_queries":
304
+ return "metrics";
305
+ }
306
+ },
307
+ PHASE_ORDER,
308
+ this.settings.resources
309
+ );
310
+ }
311
+ // -------------------------------------------------------------------------
312
+ // Fetchers
313
+ // -------------------------------------------------------------------------
314
+ async fetchAlertConditionsPage(page, signal) {
315
+ const res = await this.graphql(
316
+ ALERT_CONDITIONS_QUERY,
317
+ { accountId: this.settings.accountId, cursor: page },
318
+ "alert_conditions",
319
+ signal
320
+ );
321
+ const search = res.body.data.actor.account.alerts.nrqlConditionsSearch;
322
+ return {
323
+ items: search.nrqlConditions,
324
+ next: search.nextCursor
325
+ };
326
+ }
327
+ async fetchIncidents(options, page, signal) {
328
+ const sinceMs = options.since ? parseEpoch(options.since, "iso") : null;
329
+ const lookbackHours = this.settings.incidentsLookbackHours ?? DEFAULT_INCIDENTS_LOOKBACK_HOURS;
330
+ const fromMs = sinceMs ?? Date.now() - lookbackHours * 60 * 60 * 1e3;
331
+ const floorMs = page !== null ? Number(page) : fromMs;
332
+ const nrql = `SELECT incidentId, conditionFamilyId, policyName, conditionName, openedAt, closedAt, durationSeconds, priority, title, state, entityGuid FROM NrAiIncident WHERE openedAt > ${floorMs} ORDER BY openedAt ASC LIMIT ${INCIDENTS_NRQL_LIMIT}`;
333
+ const res = await this.graphql(
334
+ NRQL_QUERY,
335
+ { accountId: this.settings.accountId, query: nrql },
336
+ "incidents",
337
+ signal
338
+ );
339
+ const results = res.body.data.actor.account.nrql.results;
340
+ if (results.length < INCIDENTS_NRQL_LIMIT) {
341
+ return { items: results, next: null };
342
+ }
343
+ const lastOpenedAt = results[results.length - 1]?.openedAt;
344
+ const next = typeof lastOpenedAt === "number" && Number.isFinite(lastOpenedAt) && lastOpenedAt > floorMs ? String(lastOpenedAt) : null;
345
+ return { items: results, next };
346
+ }
347
+ async fetchMetrics(options, signal) {
348
+ const queries = this.settings.nrqlQueries ?? [];
349
+ if (queries.length === 0) {
350
+ return { items: [], next: null };
351
+ }
352
+ const lookbackHours = this.settings.metricsLookbackHours ?? DEFAULT_METRICS_LOOKBACK_HOURS;
353
+ const items = [];
354
+ for (const q of queries) {
355
+ signal?.throwIfAborted();
356
+ const nrql = this.appendSinceIfMissing(
357
+ q.query,
358
+ options.since ?? null,
359
+ lookbackHours
360
+ );
361
+ const res = await this.graphql(
362
+ NRQL_QUERY,
363
+ { accountId: this.settings.accountId, query: nrql },
364
+ "nrql_queries",
365
+ signal
366
+ );
367
+ items.push({
368
+ queryName: q.name,
369
+ query: q.query,
370
+ result: res.body.data.actor.account.nrql
371
+ });
372
+ }
373
+ return { items, next: null };
374
+ }
375
+ appendSinceIfMissing(query, since, fallbackHours) {
376
+ if (/\bSINCE\b/i.test(query)) {
377
+ return query;
378
+ }
379
+ const sinceMs = since ? parseEpoch(since, "iso") : null;
380
+ if (sinceMs !== null) {
381
+ return `${query} SINCE ${sinceMs}`;
382
+ }
383
+ return `${query} SINCE ${fallbackHours} hours ago`;
384
+ }
385
+ // -------------------------------------------------------------------------
386
+ // Writers
387
+ // -------------------------------------------------------------------------
388
+ async writeAlertConditions(storage, conditions) {
389
+ for (const c of conditions) {
390
+ const createdMs = c.createdAt ?? null;
391
+ const modifiedMs = c.updatedAt ?? null;
392
+ const updatedAt = modifiedMs ?? createdMs ?? Date.now();
393
+ await storage.entity({
394
+ type: "newrelic_alert_condition",
395
+ id: c.id,
396
+ attributes: {
397
+ conditionId: c.id,
398
+ name: c.name,
399
+ enabled: c.enabled,
400
+ policyId: c.policyId,
401
+ conditionType: c.type,
402
+ nrqlQuery: c.nrql?.query ?? null,
403
+ createdAt: createdMs,
404
+ modifiedAt: modifiedMs
405
+ },
406
+ updated_at: updatedAt
407
+ });
408
+ }
409
+ }
410
+ async writeIncidents(storage, rows) {
411
+ for (const row of rows) {
412
+ const incidentId = typeof row.incidentId === "string" || typeof row.incidentId === "number" ? String(row.incidentId) : null;
413
+ const openedAtRaw = row.openedAt;
414
+ const openedAtMs = typeof openedAtRaw === "number" && Number.isFinite(openedAtRaw) ? openedAtRaw : null;
415
+ if (incidentId === null || openedAtMs === null) {
416
+ continue;
417
+ }
418
+ const closedAtRaw = row.closedAt;
419
+ const closedAtMs = typeof closedAtRaw === "number" && Number.isFinite(closedAtRaw) ? closedAtRaw : null;
420
+ await storage.event({
421
+ name: "newrelic_alert_violation",
422
+ start_ts: openedAtMs,
423
+ end_ts: closedAtMs,
424
+ attributes: {
425
+ incidentId,
426
+ conditionFamilyId: this.coerceScalar(row.conditionFamilyId),
427
+ conditionName: this.coerceScalar(row.conditionName),
428
+ policyName: this.coerceScalar(row.policyName),
429
+ priority: this.coerceScalar(row.priority),
430
+ title: this.coerceScalar(row.title),
431
+ state: this.coerceScalar(row.state),
432
+ entityGuid: this.coerceScalar(row.entityGuid),
433
+ durationSeconds: this.coerceScalar(row.durationSeconds)
434
+ }
435
+ });
436
+ }
437
+ }
438
+ coerceScalar(value) {
439
+ if (value === void 0) {
440
+ return null;
441
+ }
442
+ return value;
443
+ }
444
+ async writeMetrics(storage, items) {
445
+ if (items.length === 0) {
446
+ return;
447
+ }
448
+ const samplesByName = /* @__PURE__ */ new Map();
449
+ for (const item of items) {
450
+ const facets = item.result.metadata?.facets ?? [];
451
+ for (const row of item.result.results) {
452
+ const ts = this.extractTimestamp(row, item.result);
453
+ if (ts === null) {
454
+ continue;
455
+ }
456
+ const value = this.extractValue(row);
457
+ if (value === null) {
458
+ continue;
459
+ }
460
+ const facetsStr = this.extractFacets(row, facets ?? []);
461
+ const name = `newrelic_nrql_metric.${item.queryName}`;
462
+ let bucket = samplesByName.get(name);
463
+ if (!bucket) {
464
+ bucket = [];
465
+ samplesByName.set(name, bucket);
466
+ }
467
+ bucket.push({
468
+ name,
469
+ ts,
470
+ value,
471
+ attributes: {
472
+ queryName: item.queryName,
473
+ query: item.query,
474
+ facets: facetsStr
475
+ }
476
+ });
477
+ }
478
+ }
479
+ for (const [name, samples] of samplesByName) {
480
+ await storage.metrics(samples, { names: [name] });
481
+ }
482
+ }
483
+ extractTimestamp(row, result) {
484
+ const endTimeSeconds = row.endTimeSeconds;
485
+ if (typeof endTimeSeconds === "number" && Number.isFinite(endTimeSeconds)) {
486
+ return parseEpoch(endTimeSeconds, "s");
487
+ }
488
+ const beginTimeSeconds = row.beginTimeSeconds;
489
+ if (typeof beginTimeSeconds === "number" && Number.isFinite(beginTimeSeconds)) {
490
+ return parseEpoch(beginTimeSeconds, "s");
491
+ }
492
+ const timestamp = row.timestamp;
493
+ if (typeof timestamp === "number" && Number.isFinite(timestamp)) {
494
+ return parseEpoch(timestamp, "ms");
495
+ }
496
+ const endMs = result.metadata?.timeWindow?.end;
497
+ if (typeof endMs === "number" && Number.isFinite(endMs)) {
498
+ return parseEpoch(endMs, "ms");
499
+ }
500
+ return Date.now();
501
+ }
502
+ extractValue(row) {
503
+ for (const [key, value] of Object.entries(row)) {
504
+ if (TIMESTAMP_FIELDS.has(key) || FACET_FIELDS.has(key)) {
505
+ continue;
506
+ }
507
+ if (typeof value === "number" && Number.isFinite(value)) {
508
+ return value;
509
+ }
510
+ }
511
+ return null;
512
+ }
513
+ extractFacets(row, facetNames) {
514
+ const facetRaw = row.facet;
515
+ if (Array.isArray(facetRaw)) {
516
+ return facetRaw.length > 0 ? facetRaw.map(String).join(",") : "*";
517
+ }
518
+ if (typeof facetRaw === "string" && facetRaw.length > 0) {
519
+ return facetRaw;
520
+ }
521
+ if (facetNames.length > 0) {
522
+ const parts = [];
523
+ for (const fname of facetNames) {
524
+ const v = row[fname];
525
+ if (v !== void 0 && v !== null) {
526
+ parts.push(String(v));
527
+ }
528
+ }
529
+ if (parts.length > 0) {
530
+ return parts.join(",");
531
+ }
532
+ }
533
+ return "*";
534
+ }
535
+ // -------------------------------------------------------------------------
536
+ // sync
537
+ // -------------------------------------------------------------------------
538
+ async sync(options, storage, signal) {
539
+ const cursor = isNewRelicSyncCursor(
540
+ options.cursor
541
+ ) ? options.cursor : void 0;
542
+ const isFull = options.mode === "full";
543
+ const phases = this.activePhases();
544
+ return paginateChunked({
545
+ phases,
546
+ cursor,
547
+ signal,
548
+ logger: this.logger,
549
+ fetchPage: async (phase, page, sig) => {
550
+ switch (phase) {
551
+ case "alert_conditions":
552
+ return this.fetchAlertConditionsPage(page, sig);
553
+ case "incidents":
554
+ return this.fetchIncidents(options, page, sig);
555
+ case "metrics":
556
+ return this.fetchMetrics(options, sig);
557
+ }
558
+ },
559
+ writeBatch: async (phase, items, page) => {
560
+ if (isFull && page === null) {
561
+ switch (phase) {
562
+ case "alert_conditions":
563
+ await storage.entities([], {
564
+ types: ["newrelic_alert_condition"]
565
+ });
566
+ break;
567
+ case "incidents":
568
+ await storage.events([], { names: ["newrelic_alert_violation"] });
569
+ break;
570
+ case "metrics":
571
+ for (const q of this.settings.nrqlQueries ?? []) {
572
+ await storage.metrics([], {
573
+ names: [`newrelic_nrql_metric.${q.name}`]
574
+ });
575
+ }
576
+ break;
577
+ }
578
+ }
579
+ switch (phase) {
580
+ case "alert_conditions":
581
+ return this.writeAlertConditions(
582
+ storage,
583
+ items
584
+ );
585
+ case "incidents":
586
+ return this.writeIncidents(
587
+ storage,
588
+ items
589
+ );
590
+ case "metrics":
591
+ return this.writeMetrics(storage, items);
592
+ }
593
+ }
594
+ });
595
+ }
596
+ };
597
+
598
+ // src/index.ts
599
+ var index_default = NewRelicConnector;
600
+ export {
601
+ NewRelicConnector,
602
+ configFields,
603
+ index_default as default,
604
+ doc,
605
+ id,
606
+ newRelicResources as resources
607
+ };
608
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/new-relic.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type JSONValue,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// configFields\n// ---------------------------------------------------------------------------\n\nconst nrqlQuerySchema = z.object({\n name: z\n .string()\n .min(1)\n .regex(/^[a-zA-Z0-9_]+$/, {\n message: 'Metric name must be alphanumeric / underscore',\n }),\n query: z.string().min(1),\n});\n\nexport const configFields = defineConfigFields(\n z.object({\n apiKey: z.object({ $secret: z.string().min(1) }).meta({\n label: 'User API Key',\n description:\n 'New Relic User API key. Create at New Relic -> API keys (User key type, ingest-keys do not work for NerdGraph).',\n placeholder: 'NRAK-...',\n secret: true,\n }),\n accountId: z.number().int().positive().meta({\n label: 'Account ID',\n description:\n 'New Relic account ID the User API key has access to. Find it under New Relic -> Administration -> Access management.',\n placeholder: '1234567',\n }),\n region: z.enum(['US', 'EU']).optional().meta({\n label: 'Region',\n description:\n 'New Relic data region. Defaults to `US` (`api.newrelic.com`); set to `EU` to use `api.eu.newrelic.com`.',\n placeholder: 'US',\n }),\n nrqlQueries: z.array(nrqlQuerySchema).nonempty().optional().meta({\n label: 'NRQL queries (optional)',\n description:\n 'User-declared NRQL queries. Each entry produces `newrelic_nrql_metric` samples named `<name>` from the NerdGraph NRQL API.',\n }),\n resources: z\n .array(z.enum(['alerts', 'alert_violations', 'nrql_queries']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which New Relic resources to sync. Omit to sync all of them.',\n }),\n incidentsLookbackHours: z\n .number()\n .int()\n .positive()\n .max(720)\n .optional()\n .meta({\n label: 'Incidents lookback (hours)',\n description:\n 'Window of NrAiIncident rows to pull on each sync, in hours. Defaults to 168 (7 days). Ignored when `since` is set by the host.',\n placeholder: '168',\n }),\n metricsLookbackHours: z.number().int().positive().max(168).optional().meta({\n label: 'NRQL metrics lookback (hours)',\n description:\n 'Window of NRQL metric samples to pull on each sync, in hours. Defaults to 24. Each user query gets `SINCE <lookback> hours ago` appended unless the query already declares its own `SINCE` clause.',\n placeholder: '24',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'New Relic',\n category: 'infrastructure',\n brandColor: '#1CE783',\n tagline:\n 'Sync NRQL alert conditions, AI incidents, and user-declared NRQL metric queries from a New Relic account via NerdGraph.',\n vendor: {\n name: 'New Relic',\n apiDocs: 'https://docs.newrelic.com/docs/apis/nerdgraph/',\n website: 'https://newrelic.com',\n },\n auth: {\n summary:\n 'A New Relic User API key plus the numeric account ID are required. The key is stored as a secret and used to authenticate every NerdGraph GraphQL request.',\n setup: [\n 'Open New Relic -> API keys and create a `User` key. Ingest-keys are not accepted by NerdGraph.',\n 'Find the numeric account ID under New Relic -> Administration -> Access management. The User key must have access to that account.',\n 'Store the User key as a secret and reference it from the connector config as `apiKey: secret(\"NEWRELIC_USER_KEY\")`.',\n 'Set `accountId` to the numeric account ID, and optionally `region: \"EU\"` if the data lives on the EU host (`api.eu.newrelic.com`); defaults to `US` (`api.newrelic.com`).',\n ],\n },\n rateLimit:\n 'NerdGraph enforces per-account NRQL quotas; this connector retries on 429s through the standard HTTP retry policy.',\n limitations: [\n 'APM-trace deep inspection is out of scope (not dashboard-shaped).',\n 'NRQL queries are single-shot per sync (NRQL does not support cursor pagination); large queries should narrow their `SINCE` window or use `LIMIT MAX`.',\n 'Incidents are pulled via NRQL on the NrAiIncident event type, which depends on Applied Intelligence being enabled on the account.',\n 'Only NRQL-based alert conditions are synced; legacy V1 condition types are not exposed.',\n ],\n});\n\nexport type NewRelicResource = 'alerts' | 'alert_violations' | 'nrql_queries';\n\nexport interface NewRelicNrqlQuery {\n name: string;\n query: string;\n}\n\nexport interface NewRelicSettings {\n accountId: number;\n region?: 'US' | 'EU';\n nrqlQueries?: readonly NewRelicNrqlQuery[];\n resources?: readonly NewRelicResource[];\n incidentsLookbackHours?: number;\n metricsLookbackHours?: number;\n}\n\nconst newRelicCredentials = {\n apiKey: {\n description: 'New Relic User API key',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype NewRelicCredentials = typeof newRelicCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = ['alert_conditions', 'incidents', 'metrics'] as const;\n\ntype NewRelicPhase = (typeof PHASE_ORDER)[number];\n\ntype NewRelicSyncCursor = ChunkedSyncCursor<NewRelicPhase, string>;\n\nconst isNewRelicSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\n// ---------------------------------------------------------------------------\n// API types — NerdGraph response shapes the request() pipe validates against\n// ---------------------------------------------------------------------------\n\ninterface NrqlConditionNode {\n id: string;\n name: string;\n enabled: boolean;\n policyId: string;\n type: string;\n createdAt: number | null;\n updatedAt: number | null;\n nrql?: { query: string } | null;\n}\n\ninterface NrqlConditionsSearchResult {\n nrqlConditions: NrqlConditionNode[];\n nextCursor: string | null;\n totalCount: number;\n}\n\ninterface NrqlResult {\n results: Array<Record<string, JSONValue>>;\n metadata?: {\n facets?: string[] | null;\n timeWindow?: {\n begin?: number | null;\n end?: number | null;\n } | null;\n } | null;\n}\n\ninterface AlertConditionsResponse {\n actor: {\n account: {\n alerts: {\n nrqlConditionsSearch: NrqlConditionsSearchResult;\n };\n };\n };\n}\n\ninterface NrqlResponse {\n actor: {\n account: {\n nrql: NrqlResult;\n };\n };\n}\n\ninterface MetricsBatchItem {\n queryName: string;\n query: string;\n result: NrqlResult;\n}\n\ninterface GraphQLError {\n message: string;\n path?: Array<string | number>;\n}\n\ninterface GraphQLResponse<T> {\n data?: T;\n errors?: GraphQLError[];\n}\n\n// ---------------------------------------------------------------------------\n// GraphQL documents\n// ---------------------------------------------------------------------------\n\nconst ALERT_CONDITIONS_QUERY = `\n query AlertConditions($accountId: Int!, $cursor: String) {\n actor {\n account(id: $accountId) {\n alerts {\n nrqlConditionsSearch(cursor: $cursor) {\n nrqlConditions {\n id\n name\n enabled\n policyId\n type\n createdAt\n updatedAt\n nrql { query }\n }\n nextCursor\n totalCount\n }\n }\n }\n }\n }\n`;\n\nconst NRQL_QUERY = `\n query RunNrql($accountId: Int!, $query: Nrql!) {\n actor {\n account(id: $accountId) {\n nrql(query: $query) {\n results\n metadata {\n facets\n timeWindow {\n begin\n end\n }\n }\n }\n }\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Schemas — raw response shapes per resource\n// ---------------------------------------------------------------------------\n\nconst idString = z.string().min(1);\n// NRQL result-row cells in practice are scalars (string | number | boolean | null)\n// or, for the `facet` column, an array of those scalars. The connector still\n// types these as JSONValue (which is recursive) so it can pass rows through to\n// storage attributes — but the property-test schema deliberately flattens to a\n// non-recursive shape because zodToArbitrary cannot drive recursive z.lazy()\n// schemas.\nconst nrqlScalarSchema = z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.null(),\n]);\nconst nrqlCellSchema = z.union([nrqlScalarSchema, z.array(nrqlScalarSchema)]);\n\nconst nrqlConditionNodeSchema = z.object({\n id: idString,\n name: z.string(),\n enabled: z.boolean(),\n policyId: idString,\n type: z.string(),\n createdAt: z.number().nullable(),\n updatedAt: z.number().nullable(),\n nrql: z.object({ query: z.string() }).nullable().optional(),\n});\n\nconst nrqlConditionsSearchResultSchema = z.object({\n nrqlConditions: z.array(nrqlConditionNodeSchema),\n nextCursor: z.string().nullable(),\n totalCount: z.number().int().nonnegative(),\n});\n\nconst nrqlResultSchema = z.object({\n results: z.array(z.record(z.string(), nrqlCellSchema)),\n metadata: z\n .object({\n facets: z.array(z.string()).nullable().optional(),\n timeWindow: z\n .object({\n begin: z.number().nullable().optional(),\n end: z.number().nullable().optional(),\n })\n .nullable()\n .optional(),\n })\n .nullable()\n .optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_REGION: 'US' | 'EU' = 'US';\nconst DEFAULT_INCIDENTS_LOOKBACK_HOURS = 168;\nconst DEFAULT_METRICS_LOOKBACK_HOURS = 24;\nconst INCIDENTS_NRQL_LIMIT = 5000;\nconst TIMESTAMP_FIELDS = new Set([\n 'timestamp',\n 'beginTimeSeconds',\n 'endTimeSeconds',\n 'begin_time',\n 'end_time',\n 'beginTime',\n 'endTime',\n 'openedAt',\n 'closedAt',\n]);\nconst FACET_FIELDS = new Set(['facet']);\n\n// ---------------------------------------------------------------------------\n// Resource definitions\n// ---------------------------------------------------------------------------\n\nexport const newRelicResources = defineResources({\n newrelic_alert_condition: {\n shape: 'entity',\n description:\n 'NRQL alert conditions with name, enabled state, policy id, type, and the underlying NRQL query string.',\n endpoint:\n 'GraphQL query: actor.account.alerts.nrqlConditionsSearch { nrqlConditions { ... } }',\n responses: { alert_conditions: nrqlConditionsSearchResultSchema },\n },\n newrelic_alert_violation: {\n shape: 'event',\n description:\n 'AI alert violation events. Each row from the NrAiIncident event type becomes one event with openedAt / closedAt and the underlying condition / policy metadata.',\n endpoint:\n 'GraphQL nrql() against `SELECT ... FROM NrAiIncident WHERE openedAt > ...`',\n notes:\n 'Append-only across syncs; the connector filters NrAiIncident by `openedAt` against `options.since` (or the configured lookback) to avoid re-emitting old incidents.',\n responses: { incidents: nrqlResultSchema },\n },\n newrelic_nrql_metric: {\n shape: 'metric',\n dynamic: true,\n description:\n 'User-declared NRQL metric samples, stored as `newrelic_nrql_metric.<query name>`. Each NRQL result row is mapped to a single sample using the first numeric, non-timestamp/facet field as the value.',\n endpoint: 'GraphQL nrql() against the user-declared NRQL query',\n dimensions: [\n { name: 'queryName', description: 'The user-declared query name.' },\n { name: 'query', description: 'The NRQL query string.' },\n {\n name: 'facets',\n description:\n 'Comma-joined facet values for the row, or `*` when the result row is ungrouped.',\n },\n ],\n responses: { nrql_queries: nrqlResultSchema },\n },\n});\n\n// ---------------------------------------------------------------------------\n// NewRelicConnector\n// ---------------------------------------------------------------------------\n\nexport const id = 'new-relic';\n\nexport class NewRelicConnector extends BaseConnector<\n NewRelicSettings,\n NewRelicCredentials\n> {\n static readonly id = id;\n\n static readonly resources = newRelicResources;\n\n static readonly schemas = schemasFromResources(newRelicResources);\n\n static create(input: unknown, ctx?: ConnectorContext): NewRelicConnector {\n const parsed = configFields.parse(input);\n return new NewRelicConnector(\n {\n accountId: parsed.accountId,\n region: parsed.region,\n nrqlQueries: parsed.nrqlQueries,\n resources: parsed.resources,\n incidentsLookbackHours: parsed.incidentsLookbackHours,\n metricsLookbackHours: parsed.metricsLookbackHours,\n },\n { apiKey: parsed.apiKey },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = newRelicCredentials;\n\n private get endpoint(): string {\n const region = this.settings.region ?? DEFAULT_REGION;\n return region === 'EU'\n ? 'https://api.eu.newrelic.com/graphql'\n : 'https://api.newrelic.com/graphql';\n }\n\n private buildHeaders(): Record<string, string> {\n return {\n 'API-Key': this.creds.apiKey,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent('new-relic'),\n };\n }\n\n private async graphql<T>(\n query: string,\n variables: Record<string, unknown>,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<GraphQLResponse<T>>> {\n const res = await this.post<GraphQLResponse<T>>(this.endpoint, {\n resource,\n headers: this.buildHeaders(),\n body: JSON.stringify({ query, variables }),\n signal,\n });\n if (res.body.errors && res.body.errors.length > 0) {\n const messages = res.body.errors.map((e) => e.message).join('; ');\n throw new Error(`New Relic NerdGraph error: ${messages}`);\n }\n if (!res.body.data) {\n throw new Error(\n `New Relic NerdGraph response missing data for resource '${resource}'`,\n );\n }\n return res;\n }\n\n // -------------------------------------------------------------------------\n // Resource enablement\n // -------------------------------------------------------------------------\n\n private activePhases(): NewRelicPhase[] {\n return selectActivePhases<NewRelicResource, NewRelicPhase>(\n (r) => {\n switch (r) {\n case 'alerts':\n return 'alert_conditions';\n case 'alert_violations':\n return 'incidents';\n case 'nrql_queries':\n return 'metrics';\n }\n },\n PHASE_ORDER,\n this.settings.resources,\n );\n }\n\n // -------------------------------------------------------------------------\n // Fetchers\n // -------------------------------------------------------------------------\n\n private async fetchAlertConditionsPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: NrqlConditionNode[]; next: string | null }> {\n const res = await this.graphql<AlertConditionsResponse>(\n ALERT_CONDITIONS_QUERY,\n { accountId: this.settings.accountId, cursor: page },\n 'alert_conditions',\n signal,\n );\n const search = res.body.data!.actor.account.alerts.nrqlConditionsSearch;\n return {\n items: search.nrqlConditions,\n next: search.nextCursor,\n };\n }\n\n private async fetchIncidents(\n options: SyncOptions,\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: Array<Record<string, JSONValue>>; next: string | null }> {\n const sinceMs = options.since ? parseEpoch(options.since, 'iso') : null;\n const lookbackHours =\n this.settings.incidentsLookbackHours ?? DEFAULT_INCIDENTS_LOOKBACK_HOURS;\n const fromMs = sinceMs ?? Date.now() - lookbackHours * 60 * 60 * 1000;\n const floorMs = page !== null ? Number(page) : fromMs;\n const nrql = `SELECT incidentId, conditionFamilyId, policyName, conditionName, openedAt, closedAt, durationSeconds, priority, title, state, entityGuid FROM NrAiIncident WHERE openedAt > ${floorMs} ORDER BY openedAt ASC LIMIT ${INCIDENTS_NRQL_LIMIT}`;\n const res = await this.graphql<NrqlResponse>(\n NRQL_QUERY,\n { accountId: this.settings.accountId, query: nrql },\n 'incidents',\n signal,\n );\n const results = res.body.data!.actor.account.nrql.results;\n if (results.length < INCIDENTS_NRQL_LIMIT) {\n return { items: results, next: null };\n }\n const lastOpenedAt = results[results.length - 1]?.openedAt;\n const next =\n typeof lastOpenedAt === 'number' &&\n Number.isFinite(lastOpenedAt) &&\n lastOpenedAt > floorMs\n ? String(lastOpenedAt)\n : null;\n return { items: results, next };\n }\n\n private async fetchMetrics(\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: MetricsBatchItem[]; next: string | null }> {\n const queries = this.settings.nrqlQueries ?? [];\n if (queries.length === 0) {\n return { items: [], next: null };\n }\n const lookbackHours =\n this.settings.metricsLookbackHours ?? DEFAULT_METRICS_LOOKBACK_HOURS;\n const items: MetricsBatchItem[] = [];\n for (const q of queries) {\n signal?.throwIfAborted();\n const nrql = this.appendSinceIfMissing(\n q.query,\n options.since ?? null,\n lookbackHours,\n );\n const res = await this.graphql<NrqlResponse>(\n NRQL_QUERY,\n { accountId: this.settings.accountId, query: nrql },\n 'nrql_queries',\n signal,\n );\n items.push({\n queryName: q.name,\n query: q.query,\n result: res.body.data!.actor.account.nrql,\n });\n }\n return { items, next: null };\n }\n\n private appendSinceIfMissing(\n query: string,\n since: string | null,\n fallbackHours: number,\n ): string {\n if (/\\bSINCE\\b/i.test(query)) {\n return query;\n }\n const sinceMs = since ? parseEpoch(since, 'iso') : null;\n if (sinceMs !== null) {\n return `${query} SINCE ${sinceMs}`;\n }\n return `${query} SINCE ${fallbackHours} hours ago`;\n }\n\n // -------------------------------------------------------------------------\n // Writers\n // -------------------------------------------------------------------------\n\n private async writeAlertConditions(\n storage: StorageHandle,\n conditions: NrqlConditionNode[],\n ): Promise<void> {\n for (const c of conditions) {\n const createdMs = c.createdAt ?? null;\n const modifiedMs = c.updatedAt ?? null;\n const updatedAt = modifiedMs ?? createdMs ?? Date.now();\n await storage.entity({\n type: 'newrelic_alert_condition',\n id: c.id,\n attributes: {\n conditionId: c.id,\n name: c.name,\n enabled: c.enabled,\n policyId: c.policyId,\n conditionType: c.type,\n nrqlQuery: c.nrql?.query ?? null,\n createdAt: createdMs,\n modifiedAt: modifiedMs,\n },\n updated_at: updatedAt,\n });\n }\n }\n\n private async writeIncidents(\n storage: StorageHandle,\n rows: Array<Record<string, JSONValue>>,\n ): Promise<void> {\n for (const row of rows) {\n const incidentId =\n typeof row.incidentId === 'string' || typeof row.incidentId === 'number'\n ? String(row.incidentId)\n : null;\n const openedAtRaw = row.openedAt;\n const openedAtMs =\n typeof openedAtRaw === 'number' && Number.isFinite(openedAtRaw)\n ? openedAtRaw\n : null;\n if (incidentId === null || openedAtMs === null) {\n continue;\n }\n const closedAtRaw = row.closedAt;\n const closedAtMs =\n typeof closedAtRaw === 'number' && Number.isFinite(closedAtRaw)\n ? closedAtRaw\n : null;\n await storage.event({\n name: 'newrelic_alert_violation',\n start_ts: openedAtMs,\n end_ts: closedAtMs,\n attributes: {\n incidentId,\n conditionFamilyId: this.coerceScalar(row.conditionFamilyId),\n conditionName: this.coerceScalar(row.conditionName),\n policyName: this.coerceScalar(row.policyName),\n priority: this.coerceScalar(row.priority),\n title: this.coerceScalar(row.title),\n state: this.coerceScalar(row.state),\n entityGuid: this.coerceScalar(row.entityGuid),\n durationSeconds: this.coerceScalar(row.durationSeconds),\n },\n });\n }\n }\n\n private coerceScalar(value: JSONValue | undefined): JSONValue {\n if (value === undefined) {\n return null;\n }\n return value;\n }\n\n private async writeMetrics(\n storage: StorageHandle,\n items: MetricsBatchItem[],\n ): Promise<void> {\n if (items.length === 0) {\n return;\n }\n const samplesByName: Map<\n string,\n Array<{\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number>;\n }>\n > = new Map();\n for (const item of items) {\n const facets = item.result.metadata?.facets ?? [];\n for (const row of item.result.results) {\n const ts = this.extractTimestamp(row, item.result);\n if (ts === null) {\n continue;\n }\n const value = this.extractValue(row);\n if (value === null) {\n continue;\n }\n const facetsStr = this.extractFacets(row, facets ?? []);\n const name = `newrelic_nrql_metric.${item.queryName}`;\n let bucket = samplesByName.get(name);\n if (!bucket) {\n bucket = [];\n samplesByName.set(name, bucket);\n }\n bucket.push({\n name,\n ts,\n value,\n attributes: {\n queryName: item.queryName,\n query: item.query,\n facets: facetsStr,\n },\n });\n }\n }\n for (const [name, samples] of samplesByName) {\n await storage.metrics(samples, { names: [name] });\n }\n }\n\n private extractTimestamp(\n row: Record<string, JSONValue>,\n result: NrqlResult,\n ): number | null {\n const endTimeSeconds = row.endTimeSeconds;\n if (typeof endTimeSeconds === 'number' && Number.isFinite(endTimeSeconds)) {\n return parseEpoch(endTimeSeconds, 's');\n }\n const beginTimeSeconds = row.beginTimeSeconds;\n if (\n typeof beginTimeSeconds === 'number' &&\n Number.isFinite(beginTimeSeconds)\n ) {\n return parseEpoch(beginTimeSeconds, 's');\n }\n const timestamp = row.timestamp;\n if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {\n // NRQL `timestamp` is always epoch ms.\n return parseEpoch(timestamp, 'ms');\n }\n const endMs = result.metadata?.timeWindow?.end;\n if (typeof endMs === 'number' && Number.isFinite(endMs)) {\n return parseEpoch(endMs, 'ms');\n }\n return Date.now();\n }\n\n private extractValue(row: Record<string, JSONValue>): number | null {\n for (const [key, value] of Object.entries(row)) {\n if (TIMESTAMP_FIELDS.has(key) || FACET_FIELDS.has(key)) {\n continue;\n }\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value;\n }\n }\n return null;\n }\n\n private extractFacets(\n row: Record<string, JSONValue>,\n facetNames: string[],\n ): string {\n const facetRaw = row.facet;\n if (Array.isArray(facetRaw)) {\n return facetRaw.length > 0 ? facetRaw.map(String).join(',') : '*';\n }\n if (typeof facetRaw === 'string' && facetRaw.length > 0) {\n return facetRaw;\n }\n if (facetNames.length > 0) {\n const parts: string[] = [];\n for (const fname of facetNames) {\n const v = row[fname];\n if (v !== undefined && v !== null) {\n parts.push(String(v));\n }\n }\n if (parts.length > 0) {\n return parts.join(',');\n }\n }\n return '*';\n }\n\n // -------------------------------------------------------------------------\n // sync\n // -------------------------------------------------------------------------\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor: NewRelicSyncCursor | undefined = isNewRelicSyncCursor(\n options.cursor,\n )\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n const phases = this.activePhases();\n\n return paginateChunked<NewRelicPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'alert_conditions':\n return this.fetchAlertConditionsPage(page, sig);\n case 'incidents':\n return this.fetchIncidents(options, page, sig);\n case 'metrics':\n return this.fetchMetrics(options, sig);\n }\n },\n writeBatch: async (phase, items, page) => {\n if (isFull && page === null) {\n switch (phase) {\n case 'alert_conditions':\n await storage.entities([], {\n types: ['newrelic_alert_condition'],\n });\n break;\n case 'incidents':\n await storage.events([], { names: ['newrelic_alert_violation'] });\n break;\n case 'metrics':\n for (const q of this.settings.nrqlQueries ?? []) {\n await storage.metrics([], {\n names: [`newrelic_nrql_metric.${q.name}`],\n });\n }\n break;\n }\n }\n switch (phase) {\n case 'alert_conditions':\n return this.writeAlertConditions(\n storage,\n items as NrqlConditionNode[],\n );\n case 'incidents':\n return this.writeIncidents(\n storage,\n items as Array<Record<string, JSONValue>>,\n );\n case 'metrics':\n return this.writeMetrics(storage, items as MetricsBatchItem[]);\n }\n },\n });\n }\n}\n","import { NewRelicConnector } from './new-relic';\n\nexport {\n configFields,\n doc,\n NewRelicConnector,\n id,\n newRelicResources as resources,\n} from './new-relic';\nexport type { NewRelicResource, NewRelicSettings } from './new-relic';\nexport default NewRelicConnector;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rawdash/connector-new-relic",
3
+ "version": "0.0.1",
4
+ "description": "Rawdash connector for New Relic — NRQL alert conditions, incidents, and user-declared NRQL metric queries via NerdGraph.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/rawdash/rawdash.git",
11
+ "directory": "packages/connectors/new-relic"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "@rawdash/source": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint src",
29
+ "test": "vitest run"
30
+ },
31
+ "dependencies": {
32
+ "@rawdash/core": "workspace:*",
33
+ "zod": "^4.4.3"
34
+ },
35
+ "devDependencies": {
36
+ "@rawdash/connector-shared": "workspace:*",
37
+ "@rawdash/connector-test-utils": "workspace:*",
38
+ "fast-check": "^4.8.0",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.7.2",
41
+ "vitest": "^4.1.4"
42
+ }
43
+ }