@lokalise/fastify-extras 30.4.0 → 30.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,24 @@ The `getRequestIdFastifyAppConfig()` method is exported and returns:
64
64
 
65
65
  Which can be passed to Fastify during instantiation.
66
66
 
67
+ The `getFastifyAppLoggingConfig(appLogLevel, requestLoggingLevels?)` method is exported and returns Fastify configuration for request logging. It accepts:
68
+
69
+ - `appLogLevel`, the application log level from your app configuration;
70
+ - `requestLoggingLevels` (optional), an array of log levels that should enable request logging. Defaults to `['debug', 'trace', 'info']`.
71
+
72
+ This method returns a `disableRequestLogging` configuration that:
73
+ - Enables request logging only when the app log level is in `requestLoggingLevels`;
74
+ - Automatically skips logging for service utility endpoints (`/`, `/health`, `/ready`, `/live`, `/metrics`).
75
+
76
+ Example usage:
77
+ ```typescript
78
+ import { getFastifyAppLoggingConfig } from '@lokalise/fastify-extras'
79
+
80
+ const app = fastify({
81
+ ...getFastifyAppLoggingConfig(appConfig.logLevel),
82
+ })
83
+ ```
84
+
67
85
  ### Public Healthcheck Plugin
68
86
 
69
87
  Plugin to monitor app status through public healthcheck.
@@ -384,9 +402,11 @@ The plugin decorates your Fastify instance with a `Amplitude`, which you can inj
384
402
  > "@amplitude/analytics-types": "*"
385
403
  > ```
386
404
 
387
- Additionally, you have the option to enhance the safety and accuracy of your events and properties by wrapping your `Amplitude` instance with `AmplitudeAdapter`.
405
+ **Related utilities:**
388
406
 
389
- > 📘Check [`AmplitudeAdapter.spec.ts](./lib/plugins/amplitude/amplitudePlugin.spec.ts) for a practical example
407
+ - `AmplitudeAdapter` - A type-safe wrapper that validates events using Zod schemas before sending them to Amplitude
408
+ - `FakeAmplitude` - A utility class for testing environments that doesn't send events to Amplitude
409
+ See the [Amplitude utilities section](#amplitude) for detailed documentation and examples.
390
410
 
391
411
  ### Strip Trailing Slash Plugin
392
412
 
@@ -469,6 +489,119 @@ When an uncaught exception occurs, the plugin:
469
489
 
470
490
  ## Utilities
471
491
 
492
+ ### amplitude
493
+
494
+ #### FakeAmplitude
495
+
496
+ `FakeAmplitude` is a utility class that extends `Amplitude` but doesn't send any events to Amplitude. This is useful for
497
+ testing environments or when you want to disable event tracking without changing your application code.
498
+
499
+ Example usage:
500
+
501
+ ```typescript
502
+ import { FakeAmplitude } from '@lokalise/fastify-extras'
503
+
504
+ // In your test or development environment
505
+ const amplitude = new FakeAmplitude()
506
+
507
+ // track() will not send any events
508
+ amplitude.track({
509
+ event_type: 'button_clicked',
510
+ user_id: 'user-123',
511
+ })
512
+ ```
513
+
514
+ The `track()` method returns a promise that resolves to `null` immediately, maintaining the same interface as the real
515
+ `Amplitude` class without actually sending data.
516
+
517
+ #### AmplitudeAdapter
518
+
519
+ `AmplitudeAdapter` is a type-safe wrapper around `Amplitude` that uses Zod schemas to validate events before sending them.
520
+ This ensures that all events sent to Amplitude conform to predefined schemas, catching errors at compile-time and runtime.
521
+
522
+ **Key features:**
523
+
524
+ - Type-safe event tracking with TypeScript
525
+ - Automatic validation using Zod schemas
526
+ - Prevents sending malformed events to Amplitude
527
+
528
+ **Example usage:**
529
+
530
+ ```typescript
531
+ import { z } from 'zod'
532
+ import { AmplitudeAdapter, AMPLITUDE_BASE_MESSAGE_SCHEMA, Amplitude } from '@lokalise/fastify-extras'
533
+ import type { AmplitudeMessage } from '@lokalise/fastify-extras'
534
+
535
+ // Define your event schemas
536
+ const eventSchemas = {
537
+ buttonClicked: {
538
+ schema: AMPLITUDE_BASE_MESSAGE_SCHEMA.extend({
539
+ event_type: z.literal('button_clicked'),
540
+ event_properties: z.object({
541
+ button_id: z.string(),
542
+ page: z.string(),
543
+ }),
544
+ }),
545
+ },
546
+ userSignedUp: {
547
+ schema: AMPLITUDE_BASE_MESSAGE_SCHEMA.extend({
548
+ event_type: z.literal('user_signed_up'),
549
+ event_properties: z.object({
550
+ plan: z.enum(['free', 'premium']),
551
+ }),
552
+ }),
553
+ },
554
+ } as const satisfies Record<string, AmplitudeMessage>
555
+
556
+ const eventSchemaValues = Object.values(eventSchemas)
557
+ type SupportedEvents = typeof eventSchemaValues
558
+
559
+ // Create the adapter
560
+ const amplitude = new Amplitude(true)
561
+ const amplitudeAdapter = new AmplitudeAdapter<SupportedEvents>({ amplitude })
562
+
563
+ // Track events with type safety
564
+ amplitudeAdapter.track(eventSchemas.buttonClicked, {
565
+ user_id: 'user-123',
566
+ event_properties: {
567
+ button_id: 'submit-btn',
568
+ page: '/checkout',
569
+ },
570
+ })
571
+
572
+ // With groups
573
+ amplitudeAdapter.track(eventSchemas.userSignedUp, {
574
+ user_id: 'user-456',
575
+ groups: { company: 'acme-corp' },
576
+ event_properties: {
577
+ plan: 'premium',
578
+ },
579
+ })
580
+ ```
581
+
582
+ **Schema validation:**
583
+
584
+ The `AMPLITUDE_BASE_MESSAGE_SCHEMA` provides the base schema that all events must extend. It requires:
585
+
586
+ - `event_type`: A literal string identifying the event
587
+ - `user_id`: A non-empty string or the literal `'SYSTEM'`
588
+ - `groups` (optional): A record of group names to values
589
+
590
+ When defining custom events, extend this base schema with your specific `event_type` and `event_properties`:
591
+
592
+ ```typescript
593
+ const myEventSchema = AMPLITUDE_BASE_MESSAGE_SCHEMA.extend({
594
+ event_type: z.literal('my_custom_event'),
595
+ event_properties: z.object({
596
+ // Your custom properties with validation
597
+ count: z.number().int().positive(),
598
+ status: z.enum(['active', 'inactive']),
599
+ }),
600
+ })
601
+ ```
602
+
603
+ If validation fails, a `ZodError` will be thrown, preventing invalid data from being sent to Amplitude.
604
+
472
605
  ### route-utilities
473
606
 
474
607
  #### authPreHandlers
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export { commonSyncHealthcheckPlugin } from './plugins/healthcheck/commonSyncHea
29
29
  export type { CommonSyncHealthcheckPluginOptions } from './plugins/healthcheck/commonSyncHealthcheckPlugin.ts';
30
30
  export { amplitudePlugin, type AmplitudeConfig, type CreateApiTrackingEventFn, } from './plugins/amplitude/amplitudePlugin.js';
31
31
  export { Amplitude } from './plugins/amplitude/Amplitude.js';
32
+ export { FakeAmplitude } from './plugins/amplitude/FakeAmplitude.js';
32
33
  export { AmplitudeAdapter, AMPLITUDE_BASE_MESSAGE_SCHEMA, type AmplitudeMessage, type AmplitudeAdapterDependencies, } from './plugins/amplitude/AmplitudeAdapter.js';
33
34
  export type { FastifyReplyWithPayload } from './types.js';
34
35
  export { stripTrailingSlashPlugin } from './plugins/stripTrailingSlashPlugin.js';
@@ -38,4 +39,4 @@ export { createErrorHandler, isZodError, type ErrorResponseObject } from './erro
38
39
  export type { ErrorHandlerParams, FreeformRecord } from './errors/errorHandler.js';
39
40
  export { generateJwtToken, decodeJwtToken } from './jwt-utils/tokenUtils.js';
40
41
  export { createStaticTokenAuthPreHandler } from './route-utils/authPreHandlers.js';
41
- export type { AnyFastifyInstance, CommonFastifyInstance } from './plugins/pluginsCommon.js';
42
+ export { getFastifyAppLoggingConfig, type AnyFastifyInstance, type CommonFastifyInstance, } from './plugins/pluginsCommon.js';
package/dist/index.js CHANGED
@@ -15,10 +15,12 @@ export { startupHealthcheckPlugin } from './plugins/healthcheck/startupHealthche
15
15
  export { commonSyncHealthcheckPlugin } from "./plugins/healthcheck/commonSyncHealthcheckPlugin.js";
16
16
  export { amplitudePlugin, } from './plugins/amplitude/amplitudePlugin.js';
17
17
  export { Amplitude } from './plugins/amplitude/Amplitude.js';
18
+ export { FakeAmplitude } from './plugins/amplitude/FakeAmplitude.js';
18
19
  export { AmplitudeAdapter, AMPLITUDE_BASE_MESSAGE_SCHEMA, } from './plugins/amplitude/AmplitudeAdapter.js';
19
20
  export { stripTrailingSlashPlugin } from './plugins/stripTrailingSlashPlugin.js';
20
21
  export { unhandledExceptionPlugin, commonErrorObjectResolver, } from './plugins/unhandledExceptionPlugin.js';
21
22
  export { createErrorHandler, isZodError } from './errors/errorHandler.js';
22
23
  export { generateJwtToken, decodeJwtToken } from './jwt-utils/tokenUtils.js';
23
24
  export { createStaticTokenAuthPreHandler } from './route-utils/authPreHandlers.js';
25
+ export { getFastifyAppLoggingConfig, } from './plugins/pluginsCommon.js';
24
26
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,GACf,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,2CAA2C,CAAA;AAGlD,OAAO,EACL,gCAAgC,EAChC,0BAA0B,GAC3B,MAAM,+CAA+C,CAAA;AAGtD,OAAO,EACL,qCAAqC,EACrC,+BAA+B,GAChC,MAAM,oDAAoD,CAAA;AAG3D,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,0CAA0C,CAAA;AAGjD,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,mDAAmD,CAAA;AAO1D,OAAO,EAAE,mCAAmC,EAAE,MAAM,6DAA6D,CAAA;AAEjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,EACL,yBAAyB,EACzB,kCAAkC,GACnC,MAAM,+CAA+C,CAAA;AAGtD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAM1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAA;AAO1F,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAA;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAA;AAG1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAA;AAG5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,sDAAsD,CAAA;AAGlG,OAAO,EACL,eAAe,GAGhB,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAC5D,OAAO,EACL,gBAAgB,EAChB,6BAA6B,GAG9B,MAAM,yCAAyC,CAAA;AAIhD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAA;AAEhF,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,uCAAuC,CAAA;AAG9C,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAA4B,MAAM,0BAA0B,CAAA;AAGnG,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE5E,OAAO,EAAE,+BAA+B,EAAE,MAAM,kCAAkC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,GACf,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,2CAA2C,CAAA;AAGlD,OAAO,EACL,gCAAgC,EAChC,0BAA0B,GAC3B,MAAM,+CAA+C,CAAA;AAGtD,OAAO,EACL,qCAAqC,EACrC,+BAA+B,GAChC,MAAM,oDAAoD,CAAA;AAG3D,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,0CAA0C,CAAA;AAGjD,OAAO,EACL,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,mDAAmD,CAAA;AAO1D,OAAO,EAAE,mCAAmC,EAAE,MAAM,6DAA6D,CAAA;AAEjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAEtE,OAAO,EACL,yBAAyB,EACzB,kCAAkC,GACnC,MAAM,+CAA+C,CAAA;AAGtD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAM1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAA;AAO1F,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAA;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,kDAAkD,CAAA;AAG1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAA;AAG5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,sDAAsD,CAAA;AAGlG,OAAO,EACL,eAAe,GAGhB,MAAM,wCAAwC,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAA;AACpE,OAAO,EACL,gBAAgB,EAChB,6BAA6B,GAG9B,MAAM,yCAAyC,CAAA;AAIhD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAA;AAEhF,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,uCAAuC,CAAA;AAG9C,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAA4B,MAAM,0BAA0B,CAAA;AAGnG,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE5E,OAAO,EAAE,+BAA+B,EAAE,MAAM,kCAAkC,CAAA;AAClF,OAAO,EACL,0BAA0B,GAG3B,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,7 @@
1
+ import type { AmplitudeReturn, Result } from '@amplitude/analytics-types';
2
+ import type { BaseEvent } from '@amplitude/analytics-types/lib/esm/base-event.d.ts';
3
+ import { Amplitude } from './Amplitude.ts';
4
+ export declare class FakeAmplitude extends Amplitude {
5
+ constructor();
6
+ track(_: BaseEvent): AmplitudeReturn<Result | null>;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { Amplitude } from "./Amplitude.js";
2
+ export class FakeAmplitude extends Amplitude {
3
+ constructor() {
4
+ super(false);
5
+ }
6
+ track(_) {
7
+ return {
8
+ promise: Promise.resolve(null),
9
+ };
10
+ }
11
+ }
12
+ //# sourceMappingURL=FakeAmplitude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FakeAmplitude.js","sourceRoot":"","sources":["../../../lib/plugins/amplitude/FakeAmplitude.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC1C;QACE,KAAK,CAAC,KAAK,CAAC,CAAA;IACd,CAAC;IAEQ,KAAK,CAAC,CAAY;QACzB,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;SAC/B,CAAA;IACH,CAAC;CACF"}
@@ -1,4 +1,5 @@
1
- import type { CommonLogger } from '@lokalise/node-core';
2
- import type { FastifyInstance, FastifyTypeProviderDefault, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from 'fastify';
1
+ import type { AppConfig, CommonLogger } from '@lokalise/node-core';
2
+ import type { FastifyInstance, FastifyServerOptions, FastifyTypeProviderDefault, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from 'fastify';
3
3
  export type CommonFastifyInstance = FastifyInstance<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, CommonLogger, FastifyTypeProviderDefault>;
4
4
  export type AnyFastifyInstance = FastifyInstance<any, any, any, any, any>;
5
+ export declare function getFastifyAppLoggingConfig(appLogLevel: AppConfig['logLevel'], requestLoggingLevels?: string[]): Pick<FastifyServerOptions, 'disableRequestLogging'>;
@@ -1,2 +1,12 @@
1
- export {};
1
+ // Service utility endpoints to exclude from request logging
2
+ const REQUEST_LOGGING_SKIP_PATHS = new Set(['/', '/health', '/ready', '/live', '/metrics']);
3
+ const REQUEST_LOGGING_LEVELS = ['debug', 'trace', 'info'];
4
+ export function getFastifyAppLoggingConfig(appLogLevel, requestLoggingLevels = REQUEST_LOGGING_LEVELS) {
5
+ const enableRequestLogging = requestLoggingLevels.includes(appLogLevel);
6
+ return {
7
+ disableRequestLogging: enableRequestLogging
8
+ ? (req) => REQUEST_LOGGING_SKIP_PATHS.has(req.url)
9
+ : true,
10
+ };
11
+ }
2
12
  //# sourceMappingURL=pluginsCommon.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pluginsCommon.js","sourceRoot":"","sources":["../../lib/plugins/pluginsCommon.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"pluginsCommon.js","sourceRoot":"","sources":["../../lib/plugins/pluginsCommon.ts"],"names":[],"mappings":"AAqBA,4DAA4D;AAC5D,MAAM,0BAA0B,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAA;AAC3F,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;AAEzD,MAAM,UAAU,0BAA0B,CACxC,WAAkC,EAClC,oBAAoB,GAAG,sBAAsB;IAE7C,MAAM,oBAAoB,GAAG,oBAAoB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IACvE,OAAO;QACL,qBAAqB,EAAE,oBAAoB;YACzC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,0BAA0B,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;YAClD,CAAC,CAAC,IAAI;KACT,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokalise/fastify-extras",
3
- "version": "30.4.0",
3
+ "version": "30.6.0",
4
4
  "description": "Opinionated set of fastify plugins, commonly used in Lokalise",
5
5
  "author": {
6
6
  "name": "Lokalise",
@@ -52,7 +52,7 @@
52
52
  "@lokalise/node-core": ">=14.0.0",
53
53
  "@opentelemetry/api": ">=1.9.0",
54
54
  "bullmq": "^5.19.0",
55
- "fastify": "^5.5.0",
55
+ "fastify": "^5.7.1",
56
56
  "fastify-type-provider-zod": ">=6.0.0",
57
57
  "ioredis": "^5.7.0",
58
58
  "newrelic": ">=11.13.0",
@@ -72,11 +72,11 @@
72
72
  "@types/node": "^24.7.0",
73
73
  "@vitest/coverage-v8": "^3.2.4",
74
74
  "auto-changelog": "^2.4.0",
75
- "bullmq": "^5.61.0",
76
- "fastify": "^5.6.1",
75
+ "bullmq": "^5.67.1",
76
+ "fastify": "^5.7.2",
77
77
  "fastify-type-provider-zod": "^6.0.0",
78
78
  "ioredis": "^5.6.1",
79
- "newrelic": "13.9.2",
79
+ "newrelic": "13.10.0",
80
80
  "pino": "^10.0.0",
81
81
  "pino-pretty": "^13.1.1",
82
82
  "rimraf": "^6.0.1",