@dereekb/analytics 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type AnalyticsEventName } from '@dereekb/analytics';
3
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
4
+ import { type Reflector } from '@nestjs/core';
5
+ import { type EventEmitter2 } from '@nestjs/event-emitter';
6
+ import { type Observable } from 'rxjs';
7
+ export declare const ANALYTICS_INTERCEPTOR_METADATA_KEY = "analyticsevent";
8
+ /**
9
+ * Function that extracts analytics event data from a handler result.
10
+ *
11
+ * @param result - The handler's return value.
12
+ * @param context - The NestJS execution context for the current request.
13
+ * @returns An object of event data to attach to the analytics event, or nothing if no data should be emitted.
14
+ */
15
+ export type AnalyticsEventDataFunction<T> = (result: T, context: ExecutionContext) => Maybe<object>;
16
+ /**
17
+ * Configuration for the {@link EmitAnalyticsEvent} decorator.
18
+ */
19
+ export interface AnalyticsEventInterceptorConfig<T> {
20
+ /**
21
+ * The analytics event name to emit.
22
+ */
23
+ readonly name: AnalyticsEventName;
24
+ /**
25
+ * Optional function to extract event data from the handler result.
26
+ */
27
+ readonly fn?: AnalyticsEventDataFunction<T>;
28
+ }
29
+ /**
30
+ * Decorator that marks a controller method for analytics event emission.
31
+ *
32
+ * Used in conjunction with {@link AnalyticsEventInterceptor} to emit events
33
+ * to NestJS EventEmitter2 after the handler completes.
34
+ *
35
+ * @param config - The analytics event configuration specifying the event name and optional data extractor.
36
+ * @returns A method decorator that attaches analytics metadata.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * @EmitAnalyticsEvent({ name: 'User Registered', fn: (result) => ({ userId: result.id }) })
41
+ * @Post('register')
42
+ * async register(@Body() body: RegisterDto) {
43
+ * return this.authService.register(body);
44
+ * }
45
+ * ```
46
+ */
47
+ export declare const EmitAnalyticsEvent: <T>(config: AnalyticsEventInterceptorConfig<T>) => import("@nestjs/common").CustomDecorator<string>;
48
+ /**
49
+ * NestJS interceptor that emits analytics events via EventEmitter2
50
+ * for controller methods decorated with {@link EmitAnalyticsEvent}.
51
+ */
52
+ export declare class AnalyticsEventInterceptor<T = any> implements NestInterceptor {
53
+ readonly reflector: Reflector;
54
+ readonly eventEmitter: EventEmitter2;
55
+ constructor(reflector: Reflector, eventEmitter: EventEmitter2);
56
+ /**
57
+ * Intercepts the request pipeline, emitting an analytics event after the handler completes
58
+ * if the method is decorated with {@link EmitAnalyticsEvent}.
59
+ *
60
+ * @param context - The NestJS execution context.
61
+ * @param next - The next handler in the pipeline.
62
+ * @returns An observable that emits the handler result after triggering the analytics event.
63
+ */
64
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
65
+ }
@@ -0,0 +1 @@
1
+ export * from './analytics.interceptor';
@@ -0,0 +1,5 @@
1
+ export * from './segment.api';
2
+ export * from './segment.config';
3
+ export * from './segment.module';
4
+ export * from './segment.service';
5
+ export * from './segment.type';
@@ -0,0 +1,15 @@
1
+ import { type OnModuleDestroy } from '@nestjs/common';
2
+ import { Analytics } from '@segment/analytics-node';
3
+ import { SegmentServiceConfig } from './segment.config';
4
+ /**
5
+ * Injectable wrapper around the Segment Analytics Node SDK.
6
+ *
7
+ * Manages the Analytics client lifecycle, including flushing on module destroy.
8
+ */
9
+ export declare class SegmentApi implements OnModuleDestroy {
10
+ readonly config: SegmentServiceConfig;
11
+ readonly analytics: Analytics;
12
+ constructor(config: SegmentServiceConfig);
13
+ get logOnly(): boolean;
14
+ onModuleDestroy(): Promise<void>;
15
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Application context included in Segment event contexts.
3
+ */
4
+ export interface SegmentServiceAppContext {
5
+ readonly name: string;
6
+ readonly version?: string;
7
+ readonly namespace?: string;
8
+ }
9
+ /**
10
+ * Configuration for the Segment analytics service.
11
+ */
12
+ export declare class SegmentServiceConfig {
13
+ /**
14
+ * Segment write key for the source.
15
+ */
16
+ readonly writeKey: string;
17
+ /**
18
+ * When true, events are logged to the console instead of sent to Segment.
19
+ */
20
+ readonly logOnly: boolean;
21
+ /**
22
+ * Optional application context included in all Segment event contexts.
23
+ */
24
+ readonly appContext?: SegmentServiceAppContext;
25
+ /**
26
+ * Validates that the given config has the required fields (e.g., a non-empty write key).
27
+ *
28
+ * @param config - The config instance to validate.
29
+ * @throws {Error} When the write key is missing or empty.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * SegmentServiceConfig.assertValidConfig(config);
34
+ * ```
35
+ */
36
+ static assertValidConfig(config: SegmentServiceConfig): void;
37
+ }
@@ -0,0 +1,28 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { ServerEnvironmentService } from '@dereekb/nestjs';
3
+ import { SegmentServiceConfig } from './segment.config';
4
+ /**
5
+ * Factory that creates a SegmentServiceConfig from environment variables.
6
+ *
7
+ * When a {@link ServerEnvironmentService} is provided and the current environment is a
8
+ * testing environment, `logOnly` is forced to true regardless of the env variable.
9
+ *
10
+ * @param configService - NestJS ConfigService for reading environment variables.
11
+ * @param serverEnvironmentService - Service that identifies the current server environment.
12
+ * @returns A validated {@link SegmentServiceConfig} instance.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const config = segmentServiceConfigFactory(configService, serverEnvironmentService);
17
+ * ```
18
+ */
19
+ export declare function segmentServiceConfigFactory(configService: ConfigService, serverEnvironmentService: ServerEnvironmentService): SegmentServiceConfig;
20
+ /**
21
+ * NestJS module that provides the {@link SegmentService} and its dependencies.
22
+ *
23
+ * Reads `SEGMENT_WRITE_KEY` and `SEGMENT_LOG_ONLY` from environment variables via {@link ConfigModule}.
24
+ * When a {@link ServerEnvironmentService} is available and the environment is a testing environment,
25
+ * `logOnly` is forced to `true`.
26
+ */
27
+ export declare class SegmentServiceModule {
28
+ }
@@ -0,0 +1,62 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type AnalyticsUserId } from '@dereekb/analytics';
3
+ import { SegmentApi } from './segment.api';
4
+ import { SegmentServiceConfig } from './segment.config';
5
+ import { type SegmentTrackEvent, type SegmentIdentifyParams } from './segment.type';
6
+ /**
7
+ * High-level Segment analytics service.
8
+ *
9
+ * Handles track and identify calls, merging application context and
10
+ * supporting log-only mode for development/testing.
11
+ */
12
+ export declare class SegmentService {
13
+ readonly segmentApi: SegmentApi;
14
+ readonly config: SegmentServiceConfig;
15
+ private readonly logger;
16
+ constructor(segmentApi: SegmentApi, config: SegmentServiceConfig);
17
+ /**
18
+ * Tracks an event for a user. Requires a userId.
19
+ *
20
+ * In log-only mode, events are logged to the console instead of being sent to Segment.
21
+ *
22
+ * @param userId - the user to associate with the event
23
+ * @param event - the track event containing event name, properties, and optional context
24
+ *
25
+ * @throws {Error} When userId is falsy.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * segmentService.track('uid_123', {
30
+ * event: 'Item Purchased',
31
+ * properties: { itemId: 'sku_abc', price: 29.99 }
32
+ * });
33
+ * ```
34
+ */
35
+ track(userId: AnalyticsUserId, event: SegmentTrackEvent): void;
36
+ /**
37
+ * Tracks an event only if userId is provided. No-op if userId is nullish.
38
+ *
39
+ * Convenience wrapper around {@link track} for cases where the user may not be authenticated.
40
+ *
41
+ * @param userId - the user to associate with the event, or nullish to skip
42
+ * @param event - the track event containing event name, properties, and optional context
43
+ */
44
+ tryTrack(userId: Maybe<AnalyticsUserId>, event: SegmentTrackEvent): void;
45
+ /**
46
+ * Identifies a user with optional traits, syncing user properties to Segment.
47
+ *
48
+ * In log-only mode, the identify call is logged to the console instead of being sent.
49
+ *
50
+ * @param params - the identify parameters including userId and optional traits
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * segmentService.identify({
55
+ * userId: 'uid_123',
56
+ * traits: { plan: 'premium', role: 'admin' }
57
+ * });
58
+ * ```
59
+ */
60
+ identify(params: SegmentIdentifyParams): void;
61
+ private _appContext;
62
+ }
@@ -0,0 +1,26 @@
1
+ import { type TrackParams, type IdentifyParams, type UserTraits } from '@segment/analytics-node';
2
+ import { type SegmentServiceAppContext } from './segment.config';
3
+ /**
4
+ * Segment event context derived from the SDK's TrackParams context,
5
+ * with an explicit `app` field for better typing.
6
+ */
7
+ export type SegmentEventContext = TrackParams['context'] & {
8
+ readonly app?: SegmentServiceAppContext;
9
+ };
10
+ /**
11
+ * Segment track event parameters derived from the SDK's TrackParams.
12
+ *
13
+ * Picks the commonly used fields without requiring userId/anonymousId
14
+ * (since those are provided separately by the service).
15
+ */
16
+ export type SegmentTrackEvent = Pick<TrackParams, 'event' | 'properties' | 'context' | 'timestamp'>;
17
+ /**
18
+ * Segment identify parameters derived from the SDK's IdentifyParams.
19
+ */
20
+ export type SegmentIdentifyParams = IdentifyParams;
21
+ /**
22
+ * Segment identify traits derived from the SDK's UserTraits.
23
+ *
24
+ * Includes standard fields like email, name, plus arbitrary key-value pairs.
25
+ */
26
+ export type SegmentIdentifyTraits = UserTraits;
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@dereekb/analytics",
3
+ "version": "13.4.0",
4
+ "exports": {
5
+ "./nestjs": {
6
+ "module": "./nestjs/index.esm.js",
7
+ "types": "./nestjs/index.d.ts",
8
+ "import": "./nestjs/index.cjs.mjs",
9
+ "default": "./nestjs/index.cjs.js"
10
+ },
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "module": "./index.esm.js",
14
+ "types": "./index.d.ts",
15
+ "import": "./index.cjs.mjs",
16
+ "default": "./index.cjs.js"
17
+ }
18
+ },
19
+ "peerDependencies": {
20
+ "@dereekb/nestjs": "13.4.0",
21
+ "@dereekb/util": "13.4.0",
22
+ "@nestjs/common": "^11.1.16",
23
+ "@nestjs/config": "^4.0.3",
24
+ "@nestjs/core": "^11.1.16",
25
+ "@nestjs/event-emitter": "^3.0.1",
26
+ "@segment/analytics-node": "^2.3.0",
27
+ "rxjs": "^7.8.0"
28
+ },
29
+ "module": "./index.esm.js",
30
+ "main": "./index.cjs.js",
31
+ "types": "./index.d.ts"
32
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './lib';
@@ -0,0 +1,62 @@
1
+ import { type Maybe, type PrimativeKey } from '@dereekb/util';
2
+ /**
3
+ * Name identifier for an analytics event (e.g., `'User Registered'`, `'Page Viewed'`).
4
+ */
5
+ export type AnalyticsEventName = string;
6
+ /**
7
+ * Unique identifier for an analytics user, typically a UID from the auth system.
8
+ */
9
+ export type AnalyticsUserId = string;
10
+ /**
11
+ * Key-value map of user properties sent alongside identify calls to analytics providers.
12
+ *
13
+ * Used to enrich user profiles in tools like Segment with traits such as roles, plan type, or onboarding status.
14
+ */
15
+ export interface AnalyticsUserProperties {
16
+ readonly [key: string]: PrimativeKey | boolean;
17
+ }
18
+ /**
19
+ * Represents a user for analytics identification, pairing a unique user ID with optional trait properties.
20
+ *
21
+ * Passed to analytics providers (e.g., Segment `identify()`) to associate events with a specific user.
22
+ */
23
+ export interface AnalyticsUser {
24
+ readonly user: AnalyticsUserId;
25
+ readonly properties?: AnalyticsUserProperties;
26
+ }
27
+ /**
28
+ * Key-value map of event-specific data attached to an analytics event.
29
+ *
30
+ * Sent as properties in `track()` calls to analytics providers.
31
+ */
32
+ export interface AnalyticsEventData {
33
+ readonly [key: string]: PrimativeKey | boolean;
34
+ }
35
+ /**
36
+ * Describes an analytics event with an optional name, numeric value, and arbitrary data payload.
37
+ *
38
+ * This is the core event shape emitted through analytics services and consumed by listeners like Segment.
39
+ */
40
+ export interface AnalyticsEvent {
41
+ readonly name?: AnalyticsEventName;
42
+ readonly value?: number;
43
+ readonly data?: AnalyticsEventData;
44
+ }
45
+ /**
46
+ * Extends {@link AnalyticsEvent} with an optional user association.
47
+ *
48
+ * Used to attach the current user context to each emitted event.
49
+ */
50
+ export interface UserAnalyticsEvent extends AnalyticsEvent {
51
+ readonly user?: Maybe<AnalyticsUser>;
52
+ }
53
+ /**
54
+ * Registration method used to create a new user account (e.g., `'facebook'`, `'google'`, `'email'`).
55
+ */
56
+ export type NewUserRegistrationMethod = 'facebook' | 'google' | 'email' | string;
57
+ /**
58
+ * Event data for new user registration events, requiring the registration method.
59
+ */
60
+ export interface NewUserAnalyticsEventData extends AnalyticsEventData {
61
+ readonly method: NewUserRegistrationMethod;
62
+ }
@@ -0,0 +1,55 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type AnalyticsEventData } from './analytics.event';
3
+ /**
4
+ * Configuration for {@link asAnalyticsEventData}.
5
+ */
6
+ export interface AsAnalyticsEventDataConfig {
7
+ /**
8
+ * Whether to flatten nested objects into dot-separated keys before filtering.
9
+ *
10
+ * When true, nested plain objects are recursively flattened so their primitive values
11
+ * are preserved with concatenated key paths (e.g., `{ user: { age: 25 } }` becomes `{ 'user.age': 25 }`).
12
+ *
13
+ * @defaultValue true
14
+ */
15
+ readonly flattenObjects?: boolean;
16
+ /**
17
+ * Separator for flattened key paths.
18
+ *
19
+ * Only applicable when {@link flattenObjects} is true.
20
+ *
21
+ * @defaultValue '.'
22
+ */
23
+ readonly separator?: string;
24
+ /**
25
+ * Maximum nesting depth to flatten.
26
+ *
27
+ * Only applicable when {@link flattenObjects} is true.
28
+ *
29
+ * @defaultValue Infinity (unlimited)
30
+ */
31
+ readonly maxDepth?: number;
32
+ }
33
+ /**
34
+ * Converts an arbitrary object into valid {@link AnalyticsEventData} by flattening nested objects
35
+ * and filtering out values that are not compatible with analytics providers.
36
+ *
37
+ * Processing steps:
38
+ * 1. Flattens nested plain objects into dot-separated key paths (configurable, enabled by default)
39
+ * 2. Filters out any values that are not `string`, `number`, or `boolean`
40
+ * 3. Filters out non-finite numbers (`NaN`, `Infinity`, `-Infinity`)
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * asAnalyticsEventData({ action: 'click', user: { age: 25, name: 'Jo' }, tags: [1, 2] });
45
+ * // { action: 'click', 'user.age': 25, 'user.name': 'Jo' }
46
+ *
47
+ * asAnalyticsEventData({ a: 'ok', b: { nested: 1 } }, { flattenObjects: false });
48
+ * // { a: 'ok' }
49
+ * ```
50
+ *
51
+ * @param input - The object to convert. If nullish, returns an empty object.
52
+ * @param config - Optional configuration for flattening behavior.
53
+ * @returns A new {@link AnalyticsEventData} object containing only valid analytics values.
54
+ */
55
+ export declare function asAnalyticsEventData(input: Maybe<Record<string, unknown>>, config?: AsAnalyticsEventDataConfig): AnalyticsEventData;
@@ -0,0 +1,2 @@
1
+ export * from './analytics.event';
2
+ export * from './analytics.event.util';