apitally 1.0.0-beta.4 → 1.0.0-beta.5

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 CHANGED
@@ -33,7 +33,7 @@ The SDK is an [OpenTelemetry](https://opentelemetry.io) distribution and works a
33
33
  Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out the 📚 [documentation](https://docs.apitally.io).
34
34
 
35
35
  > [!IMPORTANT]
36
- > **Upgrading from 0.x?** Version 1.0 is a full rewrite with a new setup API. See the [migration guide](MIGRATION.md) for a full 0.x to 1.x mapping.
36
+ > **Upgrading from 0.x?** Version 1.0 is a full rewrite with a new setup API. See the [migration guide](MIGRATION.md) for how to upgrade.
37
37
 
38
38
  ## Key features
39
39
 
@@ -62,13 +62,13 @@ The SDK supports **Node.js** `>= 20.6` and **Bun** `>= 1.2.0`.
62
62
 
63
63
  \* For Hono on Cloudflare Workers use our [Serverless SDK](https://github.com/apitally/apitally-js-serverless) instead.
64
64
 
65
- Apitally also supports many other web frameworks in [Python](https://github.com/apitally/apitally-py), [Go](https://github.com/apitally/apitally-go), [.NET](https://github.com/apitally/apitally-dotnet) and [Java](https://github.com/apitally/apitally-java) via our other SDKs.
65
+ Apitally also supports many other web frameworks in [Python](https://github.com/apitally/apitally-py), [Go](https://github.com/apitally/apitally-go) and [.NET](https://github.com/apitally/apitally-dotnet) via our other SDKs.
66
66
 
67
67
  ## Getting started
68
68
 
69
69
  If you don't have an Apitally account yet, first [sign up here](https://app.apitally.io/?signup). Then create an app in the Apitally dashboard. You'll see detailed setup instructions with code snippets you can copy and paste. These also include your write token.
70
70
 
71
- To install the SDK as a dependency in your project run:
71
+ Install the SDK in your project:
72
72
 
73
73
  ```bash
74
74
  npm install apitally
@@ -78,35 +78,32 @@ See the [SDK reference](https://docs.apitally.io/sdk-reference/javascript/v1/con
78
78
 
79
79
  ### AdonisJS
80
80
 
81
- Run the Ace add command from your application directory:
81
+ Run the Ace `add` command from your application directory:
82
82
 
83
83
  ```bash
84
84
  node ace add apitally
85
85
  ```
86
86
 
87
- The command installs and configures Apitally. It creates `config/apitally.ts`, adds the required environment declarations, registers the service provider and server middleware, and updates the conventional exception handler to capture validation and server errors.
88
-
89
- If needed, you can change settings in `config/apitally.ts`.
87
+ The command installs Apitally, asks for your write token and environment, and sets up the provider, middleware, and exception handling. To change settings later, edit `config/apitally.ts`.
90
88
 
91
89
  For further instructions, see our [setup guide for AdonisJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/adonisjs).
92
90
 
93
91
  ### Elysia
94
92
 
95
- Register `apitallyPlugin()` immediately after creating the app, before routes or plugins that add routes:
93
+ Register `apitallyPlugin()` immediately after creating the app, before registering plugins and routes:
96
94
 
97
95
  ```javascript
98
96
  import { Elysia } from "elysia";
99
97
  import { apitallyPlugin } from "apitally/elysia";
100
98
 
101
- const app = new Elysia()
102
- .use(
103
- apitallyPlugin({
104
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
105
- env: "dev", // optional, defaults to "dev"
106
- }),
107
- )
108
- // register plugins and routes below this point
109
- .get("/items/:id", ({ params }) => ({ id: params.id }));
99
+ const app = new Elysia().use(
100
+ apitallyPlugin({
101
+ writeToken: "your-write-token",
102
+ env: "dev",
103
+ }),
104
+ );
105
+
106
+ // register plugins and routes below this point
110
107
  ```
111
108
 
112
109
  For further instructions, see our [setup guide for Elysia](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/elysia).
@@ -124,8 +121,8 @@ import { useApitally } from "apitally";
124
121
  const app = express();
125
122
 
126
123
  useApitally(app, {
127
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
128
- env: "dev", // or "prod" etc.
124
+ writeToken: "your-write-token",
125
+ env: "dev",
129
126
  });
130
127
  ```
131
128
 
@@ -142,8 +139,8 @@ import { useApitally } from "apitally";
142
139
  const app = Fastify();
143
140
 
144
141
  useApitally(app, {
145
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
146
- env: "dev", // or "prod" etc.
142
+ writeToken: "your-write-token",
143
+ env: "dev",
147
144
  });
148
145
 
149
146
  // register plugins and routes below this point
@@ -151,30 +148,9 @@ useApitally(app, {
151
148
 
152
149
  For further instructions, see our [setup guide for Fastify](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/fastify).
153
150
 
154
- ### NestJS
155
-
156
- Call `useApitally(app)` immediately after creating the Nest application, before `app.init()` or `app.listen()`:
157
-
158
- ```javascript
159
- import { NestFactory } from "@nestjs/core";
160
- import { useApitally } from "apitally/nestjs";
161
- import { AppModule } from "./app.module.js";
162
-
163
- const app = await NestFactory.create(AppModule);
164
-
165
- useApitally(app, {
166
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
167
- env: "dev", // or "prod" etc.
168
- });
169
-
170
- await app.listen(3000);
171
- ```
172
-
173
- For further instructions, see our [setup guide for NestJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/nestjs).
174
-
175
151
  ### H3
176
152
 
177
- Add `apitallyPlugin()` when constructing the root H3 app:
153
+ Add `apitallyPlugin()` to the `plugins` of your root H3 app:
178
154
 
179
155
  ```javascript
180
156
  import { H3 } from "h3";
@@ -183,8 +159,8 @@ import { apitallyPlugin } from "apitally/h3";
183
159
  const app = new H3({
184
160
  plugins: [
185
161
  apitallyPlugin({
186
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
187
- env: "dev", // or "prod" etc.
162
+ writeToken: "your-write-token",
163
+ env: "dev",
188
164
  }),
189
165
  ],
190
166
  });
@@ -194,7 +170,7 @@ For further instructions, see our [setup guide for H3](https://docs.apitally.io/
194
170
 
195
171
  ### Hapi
196
172
 
197
- Register `apitallyPlugin()` before calling `server.initialize()` or `server.start()`:
173
+ Register `apitallyPlugin()` immediately after creating the server, before registering other plugins and routes:
198
174
 
199
175
  ```javascript
200
176
  import Hapi from "@hapi/hapi";
@@ -204,12 +180,12 @@ const server = Hapi.server({ port: 3000 });
204
180
 
205
181
  await server.register(
206
182
  apitallyPlugin({
207
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
208
- env: "dev", // or "prod" etc.
183
+ writeToken: "your-write-token",
184
+ env: "dev",
209
185
  }),
210
186
  );
211
187
 
212
- // register application plugins and routes below this point
188
+ // register other plugins and routes below this point
213
189
  await server.start();
214
190
  ```
215
191
 
@@ -226,8 +202,8 @@ import { useApitally } from "apitally";
226
202
  const app = new Hono();
227
203
 
228
204
  useApitally(app, {
229
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
230
- env: "dev", // or "prod" etc.
205
+ writeToken: "your-write-token",
206
+ env: "dev",
231
207
  });
232
208
 
233
209
  // register middleware and routes below this point
@@ -240,14 +216,14 @@ For further instructions, see our [setup guide for Hono](https://docs.apitally.i
240
216
  Call `useApitally(app)` immediately after creating the app, before registering middleware and routes:
241
217
 
242
218
  ```javascript
243
- const Koa = require("koa");
244
- const { useApitally } = require("apitally");
219
+ import Koa from "koa";
220
+ import { useApitally } from "apitally";
245
221
 
246
222
  const app = new Koa();
247
223
 
248
224
  useApitally(app, {
249
- writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
250
- env: "dev", // or "prod" etc.
225
+ writeToken: "your-write-token",
226
+ env: "dev",
251
227
  });
252
228
 
253
229
  // register middleware and routes below this point
@@ -255,11 +231,42 @@ useApitally(app, {
255
231
 
256
232
  For further instructions, see our [setup guide for Koa](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/koa).
257
233
 
234
+ ### NestJS
235
+
236
+ Call `useApitally(app)` immediately after creating the app, before calling `app.init()` or `app.listen()`:
237
+
238
+ ```javascript
239
+ import { NestFactory } from "@nestjs/core";
240
+ import { useApitally } from "apitally/nestjs";
241
+ import { AppModule } from "./app.module.js";
242
+
243
+ const app = await NestFactory.create(AppModule);
244
+
245
+ useApitally(app, {
246
+ writeToken: "your-write-token",
247
+ env: "dev",
248
+ });
249
+
250
+ await app.listen(3000);
251
+ ```
252
+
253
+ For further instructions, see our [setup guide for NestJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/nestjs).
254
+
258
255
  ## Configuration
259
256
 
260
- The write token and environment can also be provided via the `APITALLY_WRITE_TOKEN` and `APITALLY_ENV` environment variables instead of the `writeToken` and `env` options. The environment defaults to `dev`.
257
+ Pass configuration options to `useApitally()`, or to `apitallyPlugin()` for Elysia, H3, and Hapi. For AdonisJS, set them in `config/apitally.ts`. See the [SDK reference](https://docs.apitally.io/sdk-reference/javascript/v1/configuration) for all available options.
258
+
259
+ ### Environment variables
261
260
 
262
- By default, Apitally captures response headers but not request headers or request and response bodies. You can opt in with options:
261
+ To keep configuration out of your code, set these environment variables instead of passing the matching options:
262
+
263
+ - `APITALLY_WRITE_TOKEN`: the write token (`writeToken`)
264
+ - `APITALLY_ENV`: the environment name (`env`), defaults to `dev`
265
+ - `APITALLY_DISABLED`: set to `true` to disable Apitally (`disabled`)
266
+
267
+ ### Request and response data
268
+
269
+ Apitally captures response headers by default. To also capture request headers and request and response bodies, pass these options:
263
270
 
264
271
  ```javascript
265
272
  useApitally(app, {
@@ -271,45 +278,72 @@ useApitally(app, {
271
278
  });
272
279
  ```
273
280
 
274
- Sensitive values in query parameters, headers, and body fields are masked automatically based on built-in patterns, and you can add your own via the `maskQueryParams`, `maskHeaders`, and `maskBodyFields` options.
281
+ Apitally automatically masks common sensitive values, such as `password` fields and the `Authorization` header. To mask additional values, pass regular expressions to `maskQueryParams`, `maskHeaders`, and `maskBodyFields`. See our [documentation on masking](https://docs.apitally.io/sdk-reference/javascript/v1/masking) for details.
282
+
283
+ ### Exclusions and sampling
284
+
285
+ Apitally automatically excludes common health checks and static assets, such as `/healthz` and `/robots.txt`, from logs and traces. To exclude additional paths, pass regular expressions to `excludePaths`.
286
+
287
+ To capture logs and traces for only a fraction of the remaining requests, set `sampleRate`, for example to `0.1` for 10%. To decide for each request individually, pass a callback to `sampleOnRequest` or `sampleOnResponse`.
275
288
 
276
- On high-traffic applications you can capture logs and traces for only a fraction of requests by setting `sampleRate` (e.g. `0.1` for 10%), or decide per request with the `sampleOnRequest` and `sampleOnResponse` callbacks. Metrics always count every request, regardless of sampling.
289
+ Metrics always include all requests. See our [documentation on sampling](https://docs.apitally.io/sdk-reference/javascript/v1/sampling) for details.
277
290
 
278
- Application logs written via `console` and other supported loggers are captured and correlated with requests by default. Use `maskLogRecord` to transform or drop Apitally's captured copy, or opt out with `captureLogs: false`.
291
+ ### Application logs
279
292
 
280
- See the [SDK reference](https://docs.apitally.io/sdk-reference/javascript/v1/configuration) for all configuration options.
293
+ Apitally captures logs written with these loggers and correlates them with the requests they belong to:
281
294
 
282
- ## Identifying consumers and more
295
+ - `console`
296
+ - Pino
297
+ - Winston
298
+ - NestJS `Logger`
299
+ - Hapi `request.log()`
283
300
 
284
- The top-level `apitally` package provides functions you can call from anywhere in your request handling code:
301
+ To mask or drop individual log records, pass a callback to `maskLogRecord`. To turn off log capture, set `captureLogs: false`.
302
+
303
+ See our [documentation on masking](https://docs.apitally.io/sdk-reference/javascript/v1/masking#log-record-masking-callback) for details.
304
+
305
+ ## Adding context to requests
306
+
307
+ The following functions add context to the current request. Call them while handling a request, for example in a route handler, middleware, or authentication code.
308
+
309
+ To associate the request with the API consumer making it, call `setConsumer`. Only `identifier` is required. You can also pass it as a string, for example `setConsumer(user.identifier)`.
285
310
 
286
311
  ```javascript
287
- import { setConsumer, setRequestAttribute, captureException } from "apitally";
312
+ import { setConsumer } from "apitally";
288
313
 
289
- // Associate the current request with an API consumer, with custom attributes
290
314
  setConsumer({
291
315
  identifier: user.identifier,
292
316
  name: user.name,
293
317
  group: user.group,
294
318
  attributes: { plan: user.plan },
295
319
  });
320
+ ```
296
321
 
297
- // Attach a custom attribute to the current request
298
- setRequestAttribute("tenant", tenantId);
322
+ To attach custom data to the request's trace, call `setRequestAttribute`. Sampling and masking callbacks can also read these attributes.
299
323
 
300
- // Capture a handled exception for the current request
301
- captureException(error);
324
+ ```javascript
325
+ import { setRequestAttribute } from "apitally";
326
+
327
+ setRequestAttribute("tenant.id", tenantId);
302
328
  ```
303
329
 
304
- `setConsumer()` also accepts an identifier string, for example `setConsumer(user.identifier)`.
330
+ To capture an exception that your code handles itself, call `captureException`. Unhandled exceptions are captured automatically.
305
331
 
306
- For further details, check out our [documentation](https://docs.apitally.io).
332
+ ```javascript
333
+ import { captureException } from "apitally";
307
334
 
308
- ## Existing OpenTelemetry setup
335
+ try {
336
+ processOrder();
337
+ } catch (error) {
338
+ captureException(error);
339
+ }
340
+ ```
309
341
 
310
- If your app doesn't already use OpenTelemetry, you don't need to know it's there. The Apitally SDK configures OpenTelemetry automatically.
342
+ See our documentation on [custom attributes](https://docs.apitally.io/sdk-reference/javascript/v1/attributes#set-custom-attributes) and [handled exceptions](https://docs.apitally.io/sdk-reference/javascript/v1/tracing#capture-handled-exceptions) for details.
311
343
 
312
- If your app already registers its own tracer provider (e.g. via `NodeSDK`), Apitally does not replace it. Instead, you need to add the `ApitallySpanProcessor` to your provider's span processors:
344
+ ## Existing OpenTelemetry setup
345
+
346
+ If your app already registers its own tracer provider, for example with `NodeSDK`, add `ApitallySpanProcessor` to its span processors. Apitally then uses your tracer provider instead of registering its own, and keeps your existing exporters.
313
347
 
314
348
  ```javascript
315
349
  import { NodeSDK } from "@opentelemetry/sdk-node";
@@ -321,40 +355,29 @@ const sdk = new NodeSDK({
321
355
  });
322
356
  ```
323
357
 
324
- Your tracer provider's sampling settings also affect Apitally. Requests excluded by the sampler will not have request logs or traces in Apitally. Metrics still include all requests, regardless of sampling.
325
-
326
- ### Elysia's OpenTelemetry plugin
327
-
328
- When using Elysia's `@elysia/opentelemetry` plugin, register Apitally first so it adopts the OpenTelemetry SERVER span:
329
-
330
- ```javascript
331
- import { opentelemetry } from "@elysia/opentelemetry";
332
- import { Elysia } from "elysia";
333
- import { apitallyPlugin } from "apitally/elysia";
334
-
335
- const app = new Elysia()
336
- .use(apitallyPlugin(options))
337
- .use(opentelemetry());
338
- ```
339
-
340
- ## Trusted proxies
358
+ If you use versions 8 to 10 of the Sentry SDK, which register their own tracer provider, add `ApitallySpanProcessor` to the `openTelemetrySpanProcessors` option in `Sentry.init()` and enable Sentry tracing.
341
359
 
342
- If your application runs behind a reverse proxy or load balancer, configure trusted proxies in your framework so Apitally can record the real client IP for GeoIP. Apitally uses the client IP reported by your framework. It does not read forwarding headers itself to determine the client IP.
360
+ Your tracer provider's sampler also applies to Apitally. Requests it drops have no request logs or traces in Apitally, but still count in metrics.
343
361
 
344
362
  ## Graceful shutdown
345
363
 
346
- Apitally sends telemetry in batches and automatically attempts a final export during normal process shutdown.
347
-
348
- If your code calls `process.exit()`, await `shutdown()` first to give Apitally time to send buffered telemetry. Close your server and let any remaining requests finish before doing this:
364
+ If your code calls `process.exit()`, await `shutdown()` before it so Apitally can send any remaining telemetry:
349
365
 
350
366
  ```javascript
351
367
  import { shutdown } from "apitally";
352
368
 
353
- // After the server has closed and requests have finished:
354
- await shutdown();
355
- process.exit(0);
369
+ process.on("SIGTERM", () => {
370
+ server.close(async () => {
371
+ await shutdown();
372
+ process.exit(0);
373
+ });
374
+ });
356
375
  ```
357
376
 
377
+ ## Trusted proxies
378
+
379
+ If your app runs behind a reverse proxy or load balancer, configure trusted proxies in your framework, for example with Express's `trust proxy` setting or Fastify's `trustProxy` option. Apitally records the client IP your framework reports and uses it for GeoIP.
380
+
358
381
  ## Getting help
359
382
 
360
383
  If you need help please [create a new discussion](https://github.com/orgs/apitally/discussions/categories/q-a) on GitHub or email us at [support@apitally.io](mailto:support@apitally.io). We'll get back to you as soon as possible.
@@ -7,11 +7,11 @@ const require_logCapture = require("./logCapture.cjs");
7
7
  const require_logRecordExporter = require("./logRecordExporter.cjs");
8
8
  const require_logRecordProcessor = require("./logRecordProcessor.cjs");
9
9
  const require_metrics = require("./metrics.cjs");
10
+ const require_spanProcessor = require("./spanProcessor.cjs");
10
11
  const require_providers = require("./providers.cjs");
11
12
  const require_redaction = require("./redaction.cjs");
12
13
  const require_sentry = require("./sentry.cjs");
13
14
  const require_serverErrors = require("./serverErrors.cjs");
14
- const require_spanProcessor = require("./spanProcessor.cjs");
15
15
  const require_spanExporter = require("./spanExporter.cjs");
16
16
  const require_spool = require("./spool.cjs");
17
17
  const require_startup = require("./startup.cjs");
@@ -6,11 +6,11 @@ import { installConsoleCapture, installNestLoggerCapture, installPinoCapture, in
6
6
  import { ApitallyLogRecordExporter } from "./logRecordExporter.js";
7
7
  import { ApitallyLogRecordProcessor } from "./logRecordProcessor.js";
8
8
  import { MetricsPipeline } from "./metrics.js";
9
+ import { SpanPipeline, isApitallySpanProcessorDeclared, setActiveSpanPipeline, setServerSpanActivationCallback } from "./spanProcessor.js";
9
10
  import { createLoggerProvider, resolveEnvAndCreateResource, setupTracerProvider } from "./providers.js";
10
11
  import { Redaction } from "./redaction.js";
11
12
  import { installSentryEventIdRecording } from "./sentry.js";
12
13
  import { SERVER_ERROR_EVENT_NAME, drainServerErrors, resetServerErrors } from "./serverErrors.js";
13
- import { SpanPipeline, isApitallySpanProcessorDeclared, setActiveSpanPipeline, setServerSpanActivationCallback } from "./spanProcessor.js";
14
14
  import { ApitallySpanExporter } from "./spanExporter.js";
15
15
  import { Spool } from "./spool.js";
16
16
  import { emitStartupEvent } from "./startup.js";
package/dist/config.cjs CHANGED
@@ -26,6 +26,8 @@ const DEFAULT_MASK_BODY_FIELDS = [
26
26
  "auth",
27
27
  "card[-_ ]?number",
28
28
  "ccv",
29
+ "cvv",
30
+ "cvc",
29
31
  "ssn"
30
32
  ];
31
33
  const DEFAULT_EXCLUDE_PATHS = [
@@ -77,10 +79,11 @@ function isValidWriteToken(value) {
77
79
  return WRITE_TOKEN_FORMAT.test(value);
78
80
  }
79
81
  function matchesAny(patterns, value) {
80
- return patterns.some((pattern) => {
82
+ for (const pattern of patterns) {
81
83
  pattern.lastIndex = 0;
82
- return pattern.test(value);
83
- });
84
+ if (pattern.test(value)) return true;
85
+ }
86
+ return false;
84
87
  }
85
88
  function compilePatterns(defaults, userPatterns = []) {
86
89
  return [...defaults.map((pattern) => new RegExp(pattern, "i")), ...userPatterns.map((pattern) => new RegExp(pattern))];
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n return patterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(value);\n });\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.cjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n if (pattern.test(value)) {\n return true;\n }\n }\n return false;\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
package/dist/config.js CHANGED
@@ -25,6 +25,8 @@ const DEFAULT_MASK_BODY_FIELDS = [
25
25
  "auth",
26
26
  "card[-_ ]?number",
27
27
  "ccv",
28
+ "cvv",
29
+ "cvc",
28
30
  "ssn"
29
31
  ];
30
32
  const DEFAULT_EXCLUDE_PATHS = [
@@ -76,10 +78,11 @@ function isValidWriteToken(value) {
76
78
  return WRITE_TOKEN_FORMAT.test(value);
77
79
  }
78
80
  function matchesAny(patterns, value) {
79
- return patterns.some((pattern) => {
81
+ for (const pattern of patterns) {
80
82
  pattern.lastIndex = 0;
81
- return pattern.test(value);
82
- });
83
+ if (pattern.test(value)) return true;
84
+ }
85
+ return false;
83
86
  }
84
87
  function compilePatterns(defaults, userPatterns = []) {
85
88
  return [...defaults.map((pattern) => new RegExp(pattern, "i")), ...userPatterns.map((pattern) => new RegExp(pattern))];
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n return patterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(value);\n });\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n if (pattern.test(value)) {\n return true;\n }\n }\n return false;\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_spanProcessor = require("./spanProcessor.cjs");
2
3
  const require_requestAttributes = require("./requestAttributes.cjs");
3
4
  const require_exceptions = require("./exceptions.cjs");
4
- const require_spanProcessor = require("./spanProcessor.cjs");
5
5
  const require_activation = require("./activation.cjs");
6
6
  const require_consumers = require("./consumers.cjs");
7
7
  const require_adonisjs_configure = require("./adonisjs/configure.cjs");
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { ApitallySpanProcessor } from "./spanProcessor.js";
1
2
  import { setRequestAttribute } from "./requestAttributes.js";
2
3
  import { captureException } from "./exceptions.js";
3
- import { ApitallySpanProcessor } from "./spanProcessor.js";
4
4
  import { shutdown } from "./activation.js";
5
5
  import { setConsumer } from "./consumers.js";
6
6
  import { configure as configure$1 } from "./adonisjs/configure.js";
package/dist/metrics.cjs CHANGED
@@ -43,7 +43,6 @@ var MetricsPipeline = class {
43
43
  if (record.dropReason === "method" || record.dropReason === "scheme") return;
44
44
  const source = record.attributes;
45
45
  const method = source["http.request.method"] ?? source["http.method"];
46
- const scheme = source["url.scheme"] ?? source["http.scheme"];
47
46
  const route = source["http.route"];
48
47
  if (typeof route !== "string" || route === "") return;
49
48
  const attributes = { "http.route": route };
@@ -52,8 +51,6 @@ var MetricsPipeline = class {
52
51
  if (statusCode !== void 0) attributes["http.response.status_code"] = statusCode;
53
52
  const consumer = source["apitally.consumer.identifier"];
54
53
  if (consumer !== void 0) attributes["apitally.consumer.identifier"] = consumer;
55
- if (scheme !== void 0) attributes["url.scheme"] = scheme;
56
- if (typeof statusCode === "number" && statusCode >= 500) attributes["error.type"] = String(statusCode);
57
54
  const key = JSON.stringify(attributes);
58
55
  let histograms = this.combinations.get(key);
59
56
  if (histograms === void 0) {