@equinor/fusion-framework-module-analytics 3.1.0 → 3.1.2

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.
Files changed (44) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/dist/types/collectors/create-schema.d.ts +2 -2
  4. package/dist/types/version.d.ts +1 -1
  5. package/package.json +11 -8
  6. package/CHANGELOG.md +0 -386
  7. package/docs/adapters.md +0 -79
  8. package/docs/collectors.md +0 -91
  9. package/docs/testing.md +0 -87
  10. package/docs/tracking-events.md +0 -16
  11. package/src/AnalyticsConfigurator.interface.ts +0 -77
  12. package/src/AnalyticsConfigurator.ts +0 -128
  13. package/src/AnalyticsProvider.interface.ts +0 -44
  14. package/src/AnalyticsProvider.ts +0 -150
  15. package/src/__tests__/MockAnalyticsAdapter.test.ts +0 -127
  16. package/src/__tests__/mock/analytics-mock-adapter.test.ts +0 -79
  17. package/src/adapters/AnalyticsAdapter.interface.ts +0 -36
  18. package/src/adapters/ConsoleAnalyticsAdapter.ts +0 -37
  19. package/src/adapters/FusionAnalyticsAdapter.ts +0 -95
  20. package/src/adapters/index.ts +0 -11
  21. package/src/collectors/AnalyticsCollector.interface.ts +0 -27
  22. package/src/collectors/AppLoadedCollector.ts +0 -94
  23. package/src/collectors/AppSelectedCollector.ts +0 -70
  24. package/src/collectors/ContextSelectedCollector.ts +0 -83
  25. package/src/collectors/create-schema.ts +0 -123
  26. package/src/collectors/index.ts +0 -12
  27. package/src/collectors/utils/app-key-schema.ts +0 -17
  28. package/src/collectors/utils/app-schema.ts +0 -22
  29. package/src/collectors/utils/context-schema.ts +0 -22
  30. package/src/collectors/utils/extract-app-key-metadata.ts +0 -15
  31. package/src/collectors/utils/extract-app-metadata.ts +0 -20
  32. package/src/collectors/utils/extract-context-metadata.ts +0 -19
  33. package/src/enable-analytics.ts +0 -44
  34. package/src/index.ts +0 -30
  35. package/src/logExporters/fusionOTLPLogExporter/FusionOTLPLogExporter.ts +0 -71
  36. package/src/logExporters/fusionOTLPLogExporter/HttpClientExporterTransport.ts +0 -123
  37. package/src/logExporters/index.ts +0 -10
  38. package/src/mock/MockAnalyticsAdapter.ts +0 -178
  39. package/src/mock/index.ts +0 -29
  40. package/src/module.ts +0 -59
  41. package/src/types.ts +0 -60
  42. package/src/version.ts +0 -2
  43. package/tsconfig.json +0 -29
  44. package/vitest.config.ts +0 -11
package/docs/testing.md DELETED
@@ -1,87 +0,0 @@
1
- # Testing
2
-
3
- Use `MockAnalyticsAdapter` from `@equinor/fusion-framework-module-analytics/mock` to assert on tracked analytics events without exporting them to a real backend. Register it like any other adapter via `setAdapter`, then query or await recorded events from your test:
4
-
5
- ```ts
6
- import { enableAnalytics } from '@equinor/fusion-framework-module-analytics';
7
- import { MockAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/mock';
8
-
9
- const recorder = new MockAnalyticsAdapter();
10
-
11
- enableAnalytics(configurator, (builder) => {
12
- builder.setAdapter('mock', async () => recorder);
13
- });
14
-
15
- // ... exercise the app under test, then assert:
16
- const event = await recorder.waitForAnalytic('button-click');
17
- expect(event.attributes?.section).toBe('header');
18
-
19
- // or synchronously inspect everything recorded so far:
20
- expect(recorder.getAnalytics('page-view')).toHaveLength(1);
21
- ```
22
-
23
- `MockAnalyticsAdapter` is a genuine `IAnalyticsAdapter` implementation — it works the same way through the real `enableAnalytics` configuration pipeline as `ConsoleAnalyticsAdapter` or `FusionAnalyticsAdapter`, so registering it alongside other adapters doesn't change their behavior.
24
-
25
- ## `getAnalytics(matcher?)`
26
-
27
- Returns recorded events synchronously, filtered by an event name, an array of names, or a predicate. Omit the matcher to get every recorded event.
28
-
29
- ```ts
30
- recorder.getAnalytics(); // every recorded event
31
- recorder.getAnalytics('button-click'); // by name
32
- recorder.getAnalytics(['button-click', 'page-view']); // any of these names
33
- recorder.getAnalytics((event) => event.attributes?.section === 'header'); // predicate
34
- ```
35
-
36
- ## `waitForAnalytic(matcher, options?)`
37
-
38
- Resolves with the first matching event — immediately if one was already recorded, or waiting for a future one. Supports an optional `timeout` (ms) and `signal` (`AbortSignal`) so a test can't hang indefinitely, and rejects if the adapter is disposed before a match occurs.
39
-
40
- ```ts
41
- // Rejects after 1000ms if the event never fires
42
- const event = await recorder.waitForAnalytic('button-click', { timeout: 1000 });
43
-
44
- // Rejects as soon as the signal aborts
45
- const controller = new AbortController();
46
- const pending = recorder.waitForAnalytic('button-click', { signal: controller.signal });
47
- controller.abort();
48
-
49
- await expect(pending).rejects.toThrow();
50
- ```
51
-
52
- ## Using a bespoke `ModulesConfigurator`
53
-
54
- `MockAnalyticsAdapter` works the same way with a manually composed set of modules — no app or portal host required:
55
-
56
- ```ts
57
- import { ModulesConfigurator } from '@equinor/fusion-framework-module';
58
- import { enableAnalytics } from '@equinor/fusion-framework-module-analytics';
59
- import { MockAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/mock';
60
- import type { IAnalyticsProvider } from '@equinor/fusion-framework-module-analytics';
61
-
62
- const recorder = new MockAnalyticsAdapter();
63
- const configurator = new ModulesConfigurator([]);
64
-
65
- enableAnalytics(configurator, (builder) => {
66
- builder.setAdapter('mock', async () => recorder);
67
- });
68
-
69
- // enableAnalytics only registers the module at runtime, so `initialize()` isn't
70
- // statically typed with an `analytics` property — cast to the real provider type.
71
- const instances = await configurator.initialize();
72
- const { analytics } = instances as unknown as { analytics: IAnalyticsProvider };
73
- analytics.trackAnalytic({ name: 'button-click', value: 'save' });
74
-
75
- expect(recorder.getAnalytics('button-click')).toHaveLength(1);
76
- ```
77
-
78
- ## Disposal
79
-
80
- `MockAnalyticsAdapter` completes its internal event stream on `[Symbol.dispose]()`, rejecting any pending `waitForAnalytic` calls instead of leaving them hanging:
81
-
82
- ```ts
83
- const pending = recorder.waitForAnalytic('button-click');
84
- recorder[Symbol.dispose]();
85
-
86
- await expect(pending).rejects.toThrow('disposed before a matching event was recorded');
87
- ```
@@ -1,16 +0,0 @@
1
- # Tracking Events Manually
2
-
3
- The provider exposes methods for ad-hoc event tracking outside of collectors:
4
-
5
- ```typescript
6
- // Single event
7
- provider.trackAnalytic({
8
- name: 'button-click',
9
- value: 'save',
10
- attributes: { section: 'toolbar' },
11
- });
12
-
13
- // Observable stream
14
- const subscription = provider.trackAnalytic$(myEvent$);
15
- // later: subscription.unsubscribe();
16
- ```
@@ -1,77 +0,0 @@
1
- import type { ConfigBuilderCallback } from '@equinor/fusion-framework-module';
2
- import type { IAnalyticsCollector } from './collectors/AnalyticsCollector.interface.js';
3
- import type { IAnalyticsAdapter } from './adapters/AnalyticsAdapter.interface.js';
4
- import type { AnalyticsEvent } from './types.js';
5
-
6
- /**
7
- * Resolved analytics configuration containing the instantiated collectors and adapters.
8
- *
9
- * @remarks
10
- * This type is the output of the configuration stage. The module’s `initialize`
11
- * function receives the resolved `AnalyticsConfig` and passes it to
12
- * {@link AnalyticsProvider}.
13
- */
14
- export type AnalyticsConfig = {
15
- /** Record of analytics collectors keyed by a unique identifier. */
16
- collectors: Record<string, IAnalyticsCollector>;
17
- /** Record of analytics adapters keyed by a unique identifier. */
18
- adapters: Record<string, IAnalyticsAdapter>;
19
- };
20
-
21
- /**
22
- * Configuration-time interface for registering analytics adapters and collectors.
23
- *
24
- * @remarks
25
- * Obtain an instance of this interface inside the callback passed to
26
- * {@link enableAnalytics}. Use {@link IAnalyticsConfigurator.setAdapter | setAdapter}
27
- * and {@link IAnalyticsConfigurator.setCollector | setCollector} to register
28
- * components. Both methods support method chaining.
29
- *
30
- * @example
31
- * ```ts
32
- * enableAnalytics(configurator, (builder) => {
33
- * builder
34
- * .setAdapter('console', async () => new ConsoleAnalyticsAdapter())
35
- * .setCollector('context-selected', async (args) => {
36
- * const ctx = await args.requireInstance('context');
37
- * const app = await args.requireInstance('app');
38
- * return new ContextSelectedCollector(ctx, app);
39
- * });
40
- * });
41
- * ```
42
- */
43
- export interface IAnalyticsConfigurator {
44
- /**
45
- * Registers an analytics collector factory identified by a unique key.
46
- *
47
- * The factory callback receives module-resolution arguments so it can
48
- * resolve dependencies (e.g. context or app providers) before returning
49
- * the collector instance.
50
- *
51
- * @template T - The concrete analytics event type the collector emits.
52
- * @param identifier - Unique key for this collector.
53
- * @param callBack - Async factory that returns the collector instance.
54
- * @returns The configurator instance for method chaining.
55
- */
56
- setCollector<T extends AnalyticsEvent>(
57
- identifier: string,
58
- callBack: ConfigBuilderCallback<IAnalyticsCollector<T>>,
59
- ): this;
60
-
61
- /**
62
- * Registers an analytics adapter factory identified by a unique key.
63
- *
64
- * The factory callback receives module-resolution arguments so it can
65
- * resolve dependencies (e.g. service-discovery HTTP clients) before
66
- * returning the adapter instance.
67
- *
68
- * @template T - The concrete analytics event type the adapter handles.
69
- * @param identifier - Unique key for this adapter.
70
- * @param callback - Async factory that returns the adapter instance.
71
- * @returns The configurator instance for method chaining.
72
- */
73
- setAdapter<T extends AnalyticsEvent>(
74
- identifier: string,
75
- callback: ConfigBuilderCallback<IAnalyticsAdapter<T>>,
76
- ): this;
77
- }
@@ -1,128 +0,0 @@
1
- import {
2
- BaseConfigBuilder,
3
- type ConfigBuilderCallbackArgs,
4
- type ConfigBuilderCallback,
5
- } from '@equinor/fusion-framework-module';
6
- import type { IAnalyticsConfigurator, AnalyticsConfig } from './AnalyticsConfigurator.interface.js';
7
- import type { IAnalyticsCollector } from './collectors/AnalyticsCollector.interface.js';
8
- import type { IAnalyticsAdapter } from './adapters/AnalyticsAdapter.interface.js';
9
- import type { AnalyticsEvent } from './types.js';
10
- import { from, type ObservableInput } from 'rxjs';
11
-
12
- import { map, scan, filter, defaultIfEmpty, shareReplay, mergeMap } from 'rxjs/operators';
13
-
14
- /**
15
- * Default {@link IAnalyticsConfigurator} implementation for registering analytics adapters and collectors.
16
- *
17
- * @remarks
18
- * Extends `BaseConfigBuilder` to resolve adapter and collector factories
19
- * asynchronously at module-initialisation time using RxJS `mergeMap`. Each
20
- * registered factory is invoked with the module’s `ConfigBuilderCallbackArgs`
21
- * so it can resolve dependencies from the framework before returning its
22
- * adapter or collector instance.
23
- *
24
- * Use {@link AnalyticsConfigurator.setAdapter | setAdapter} and
25
- * {@link AnalyticsConfigurator.setCollector | setCollector} to register
26
- * components. Both methods return `this` for fluent chaining.
27
- *
28
- * @see {@link IAnalyticsConfigurator}
29
- * @see {@link BaseConfigBuilder}
30
- */
31
- export class AnalyticsConfigurator
32
- extends BaseConfigBuilder<AnalyticsConfig>
33
- implements IAnalyticsConfigurator
34
- {
35
- #collectorCallbacks: Record<string, ConfigBuilderCallback<IAnalyticsCollector>> = {};
36
- #adapterCallbacks: Record<string, ConfigBuilderCallback<IAnalyticsAdapter>> = {};
37
-
38
- /**
39
- * Registers the async resolvers for `collectors` and `adapters` config keys.
40
- */
41
- constructor() {
42
- super();
43
-
44
- // Configure async collectors resolution using mergeMap to handle Promise/Observable collector factories
45
- this._set(
46
- 'collectors',
47
- (args: ConfigBuilderCallbackArgs): ObservableInput<Record<string, IAnalyticsCollector>> => {
48
- // Resolve each collector factory and drop any that resolve to a falsy value
49
- return from(Object.entries(this.#collectorCallbacks)).pipe(
50
- mergeMap(([identifier, collectorFn]) =>
51
- // Resolve the collector's own Promise/Observable factory result
52
- from(collectorFn(args)).pipe(
53
- filter((collector): collector is IAnalyticsCollector => !!collector),
54
- map((collector) => [identifier, collector] as const),
55
- ),
56
- ),
57
- scan(
58
- (acc, [identifier, collector]) => {
59
- acc[identifier] = collector;
60
- return acc;
61
- },
62
- {} as Record<string, IAnalyticsCollector>,
63
- ),
64
- defaultIfEmpty({}),
65
- shareReplay({ bufferSize: 1, refCount: true }),
66
- );
67
- },
68
- );
69
-
70
- // Configure async adapters resolution using mergeMap to handle Promise/Observable adapter factories
71
- this._set(
72
- 'adapters',
73
- (args: ConfigBuilderCallbackArgs): ObservableInput<Record<string, IAnalyticsAdapter>> => {
74
- // Resolve each adapter factory and drop any that resolve to a falsy value
75
- return from(Object.entries(this.#adapterCallbacks)).pipe(
76
- mergeMap(([identifier, adapterFn]) =>
77
- // Resolve the adapter's own Promise/Observable factory result
78
- from(adapterFn(args)).pipe(
79
- filter((adapter): adapter is IAnalyticsAdapter => !!adapter),
80
- map((adapter) => [identifier, adapter] as const),
81
- ),
82
- ),
83
- scan(
84
- (acc, [identifier, adapter]) => {
85
- acc[identifier] = adapter;
86
- return acc;
87
- },
88
- {} as Record<string, IAnalyticsAdapter>,
89
- ),
90
- defaultIfEmpty({}),
91
- shareReplay({ bufferSize: 1, refCount: true }),
92
- );
93
- },
94
- );
95
- }
96
-
97
- /**
98
- * Registers an analytics collector factory.
99
- *
100
- * @template T - The concrete analytics event type the collector emits.
101
- * @param identifier - Unique key for the collector.
102
- * @param callback - Factory that receives module-resolution args and returns a collector.
103
- * @returns `this` for method chaining.
104
- */
105
- setCollector<T extends AnalyticsEvent>(
106
- identifier: string,
107
- callback: ConfigBuilderCallback<IAnalyticsCollector<T>>,
108
- ): this {
109
- this.#collectorCallbacks[identifier] = callback;
110
- return this;
111
- }
112
-
113
- /**
114
- * Registers an analytics adapter factory.
115
- *
116
- * @template T - The concrete analytics event type the adapter handles.
117
- * @param identifier - Unique key for the adapter.
118
- * @param callback - Factory that receives module-resolution args and returns an adapter.
119
- * @returns `this` for method chaining.
120
- */
121
- setAdapter<T extends AnalyticsEvent>(
122
- identifier: string,
123
- callback: ConfigBuilderCallback<IAnalyticsAdapter<T>>,
124
- ): this {
125
- this.#adapterCallbacks[identifier] = callback;
126
- return this;
127
- }
128
- }
@@ -1,44 +0,0 @@
1
- import type { ObservableInput, Subscription } from 'rxjs';
2
- import type { AnalyticsEvent } from './types.js';
3
-
4
- /**
5
- * Public interface for the analytics provider exposed by the `analytics` module.
6
- *
7
- * @remarks
8
- * Use `trackAnalytic` to push a single event or `trackAnalytic$` to forward
9
- * an observable stream of events. Both methods route events to all registered
10
- * adapters.
11
- */
12
- export interface IAnalyticsProvider {
13
- /**
14
- * Pushes a single analytics event to all registered adapters.
15
- *
16
- * @param event - The analytics event to track.
17
- *
18
- * @example
19
- * ```ts
20
- * provider.trackAnalytic({
21
- * name: 'button-click',
22
- * value: 'save',
23
- * attributes: { page: 'settings' },
24
- * });
25
- * ```
26
- */
27
- trackAnalytic(event: AnalyticsEvent): void;
28
-
29
- /**
30
- * Subscribes to an observable stream of analytics events and forwards each
31
- * emission to all registered adapters.
32
- *
33
- * @param analytic$ - Observable input stream of analytics events.
34
- * @returns A combined `Disposable & Subscription` handle for cleanup.
35
- *
36
- * @example
37
- * ```ts
38
- * const sub = provider.trackAnalytic$(myEvent$);
39
- * // later
40
- * sub.unsubscribe();
41
- * ```
42
- */
43
- trackAnalytic$(analytic$: ObservableInput<AnalyticsEvent>): Disposable & Subscription;
44
- }
@@ -1,150 +0,0 @@
1
- import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
2
-
3
- import { version } from './version.js';
4
-
5
- import type { AnalyticsConfig } from './AnalyticsConfigurator.interface.js';
6
- import type { IAnalyticsProvider } from './AnalyticsProvider.interface.js';
7
- import type { AnalyticsEvent } from './types.js';
8
- import { from, type ObservableInput, Subject, Subscription } from 'rxjs';
9
- import type { IAnalyticsCollector } from './collectors/AnalyticsCollector.interface.js';
10
- import type { IAnalyticsAdapter } from './adapters/AnalyticsAdapter.interface.js';
11
-
12
- /**
13
- * An RxJS `Subscription` that also implements the TC39 `Disposable` protocol.
14
- *
15
- * @remarks
16
- * Returned by {@link AnalyticsProvider.trackAnalytic$} so callers can clean up
17
- * with either `subscription.unsubscribe()` or `using` / `Symbol.dispose`.
18
- */
19
- class DisposableSubscription extends Subscription {
20
- /**
21
- * @param subscription - The underlying subscription to wrap with `Symbol.dispose` support.
22
- */
23
- constructor(subscription: Subscription) {
24
- super(subscription.unsubscribe);
25
- }
26
-
27
- /** TC39 `Disposable` protocol support, delegating to `unsubscribe()`. */
28
- [Symbol.dispose] = () => {
29
- this.unsubscribe();
30
- };
31
- }
32
-
33
- /**
34
- * Runtime analytics provider that collects events from collectors and dispatches
35
- * them to adapters.
36
- *
37
- * @remarks
38
- * Created by the analytics module during initialisation. The provider:
39
- *
40
- * 1. Initialises all registered adapters and collectors.
41
- * 2. Subscribes to every collector and merges their events into a shared stream.
42
- * 3. Forwards each emitted event to every registered adapter via
43
- * {@link IAnalyticsAdapter.registerAnalytic}.
44
- *
45
- * Consumers can also push ad-hoc events with {@link AnalyticsProvider.trackAnalytic}
46
- * or subscribe an observable stream with {@link AnalyticsProvider.trackAnalytic$}.
47
- *
48
- * @extends BaseModuleProvider<AnalyticsConfig>
49
- * @implements IAnalyticsProvider
50
- */
51
- export class AnalyticsProvider
52
- extends BaseModuleProvider<AnalyticsConfig>
53
- implements IAnalyticsProvider
54
- {
55
- #analytics: Subject<AnalyticsEvent>;
56
- #collectors: Record<string, IAnalyticsCollector>;
57
- #adapters: Record<string, IAnalyticsAdapter>;
58
-
59
- /**
60
- * @param config - Resolved analytics module configuration, including collectors and adapters.
61
- */
62
- constructor(config: AnalyticsConfig) {
63
- super({ version, config });
64
-
65
- this.#analytics = new Subject();
66
- this.#collectors = config.collectors;
67
- this.#adapters = config.adapters;
68
- }
69
-
70
- /**
71
- * Initialises all adapters and collectors, then wires collector output into
72
- * the adapter pipeline.
73
- *
74
- * @remarks
75
- * Initialisation is idempotent within the module lifecycle — the module calls
76
- * this once during `module.initialize`. Steps:
77
- *
78
- * 1. Initialise all collectors (via `Promise.allSettled`).
79
- * 2. Initialise all adapters (via `Promise.allSettled`).
80
- * 3. Subscribe to each collector and forward events into the shared subject.
81
- * 4. Subscribe to the shared subject and dispatch to every adapter.
82
- *
83
- * All subscriptions are registered as teardowns on the base provider.
84
- *
85
- * @returns A promise that resolves when all adapters and collectors are initialised.
86
- */
87
- async initialize(): Promise<void> {
88
- // Kick off initialization for every collector, tolerating individual failures
89
- const initializedCollectors = Object.values(this.#collectors).map((collector) =>
90
- Promise.resolve(collector.initialize?.()),
91
- );
92
- // Kick off initialization for every adapter, tolerating individual failures
93
- const initializedAdapters = Object.values(this.#adapters).map((adapters) =>
94
- Promise.resolve(adapters.initialize?.()),
95
- );
96
-
97
- await Promise.allSettled(initializedCollectors);
98
- await Promise.allSettled(initializedAdapters);
99
-
100
- // Forward every collector's emissions into the shared analytics subject
101
- for (const collector of Object.values(this.#collectors)) {
102
- const subscription = collector.subscribe({
103
- next: (event) => {
104
- this.#analytics.next(event);
105
- },
106
- });
107
-
108
- this._addTeardown(subscription);
109
- }
110
-
111
- const adapterSubscription = this.#analytics.subscribe({
112
- next: (event) => {
113
- // Dispatch every emitted event to all registered adapters
114
- for (const adapter of Object.values(this.#adapters)) {
115
- adapter.registerAnalytic(event);
116
- }
117
- },
118
- });
119
- this._addTeardown(adapterSubscription);
120
- }
121
-
122
- /**
123
- * Pushes a single analytics event to all registered adapters.
124
- *
125
- * @param event - The analytics event to track.
126
- */
127
- trackAnalytic(event: AnalyticsEvent): void {
128
- // TODO(#5098): Validate AnalyticsEvent includes name, value and attributes
129
- this.#analytics.next(event);
130
- }
131
-
132
- /**
133
- * Subscribes to an observable stream of analytics events and forwards each
134
- * emission to all registered adapters.
135
- *
136
- * @param analytic$ - Observable input stream of analytics events.
137
- * @returns A {@link DisposableSubscription} supporting both `unsubscribe()` and `Symbol.dispose`.
138
- */
139
- trackAnalytic$(analytic$: ObservableInput<AnalyticsEvent>): Disposable & Subscription {
140
- const subscription = from(analytic$)
141
- // TODO(#5098): Validate AnalyticsEvent includes name, value and attributes
142
- .subscribe({
143
- next: (event) => {
144
- this.#analytics.next(event);
145
- },
146
- });
147
-
148
- return new DisposableSubscription(subscription);
149
- }
150
- }
@@ -1,127 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
-
3
- import { MockAnalyticsAdapter } from '../mock/MockAnalyticsAdapter.js';
4
- import type { AnalyticsEvent } from '../types.js';
5
-
6
- const createEvent = (name: string, overrides: Partial<AnalyticsEvent> = {}): AnalyticsEvent => ({
7
- name,
8
- value: null,
9
- ...overrides,
10
- });
11
-
12
- describe('MockAnalyticsAdapter', () => {
13
- afterEach(() => {
14
- vi.useRealTimers();
15
- });
16
-
17
- it('records events via registerAnalytic and returns them from getAnalytics', () => {
18
- const adapter = new MockAnalyticsAdapter();
19
-
20
- adapter.registerAnalytic(createEvent('button-click'));
21
- adapter.registerAnalytic(createEvent('page-view'));
22
-
23
- expect(adapter.getAnalytics().map((e) => e.name)).toEqual(['button-click', 'page-view']);
24
- });
25
-
26
- it('filters getAnalytics by a single name, an array, and a predicate', () => {
27
- const adapter = new MockAnalyticsAdapter();
28
- adapter.registerAnalytic(createEvent('button-click', { attributes: { section: 'header' } }));
29
- adapter.registerAnalytic(createEvent('page-view'));
30
-
31
- expect(adapter.getAnalytics('button-click')).toHaveLength(1);
32
- expect(adapter.getAnalytics(['button-click', 'page-view'])).toHaveLength(2);
33
- expect(adapter.getAnalytics((e) => e.attributes?.section === 'header')).toHaveLength(1);
34
- });
35
-
36
- it('resolves waitForAnalytic immediately when a matching event is already recorded', async () => {
37
- const adapter = new MockAnalyticsAdapter();
38
- adapter.registerAnalytic(createEvent('button-click'));
39
-
40
- const event = await adapter.waitForAnalytic('button-click');
41
-
42
- expect(event.name).toBe('button-click');
43
- });
44
-
45
- it('resolves waitForAnalytic when a matching event is recorded later', async () => {
46
- const adapter = new MockAnalyticsAdapter();
47
-
48
- const promise = adapter.waitForAnalytic('button-click');
49
- adapter.registerAnalytic(createEvent('page-view'));
50
- adapter.registerAnalytic(createEvent('button-click'));
51
- const event = await promise;
52
-
53
- expect(event.name).toBe('button-click');
54
- });
55
-
56
- it('resolves waitForAnalytic via predicate matcher', async () => {
57
- const adapter = new MockAnalyticsAdapter();
58
-
59
- const promise = adapter.waitForAnalytic((e) => e.attributes?.id === 42);
60
- adapter.registerAnalytic(createEvent('button-click', { attributes: { id: 1 } }));
61
- adapter.registerAnalytic(createEvent('button-click', { attributes: { id: 42 } }));
62
- const event = await promise;
63
-
64
- expect(event.attributes?.id).toBe(42);
65
- });
66
-
67
- it('rejects when the timeout elapses before a matching event is recorded', async () => {
68
- vi.useFakeTimers();
69
- const adapter = new MockAnalyticsAdapter();
70
-
71
- const promise = adapter.waitForAnalytic('button-click', { timeout: 500 });
72
- vi.advanceTimersByTime(501);
73
-
74
- await expect(promise).rejects.toThrow('waitForAnalytic timed out after 500ms');
75
- });
76
-
77
- it('rejects when the AbortSignal fires before a matching event', async () => {
78
- const adapter = new MockAnalyticsAdapter();
79
- const controller = new AbortController();
80
-
81
- const promise = adapter.waitForAnalytic('button-click', { signal: controller.signal });
82
- controller.abort();
83
-
84
- await expect(promise).rejects.toThrow();
85
- });
86
-
87
- it('rejects immediately when passed an already-aborted signal', async () => {
88
- const adapter = new MockAnalyticsAdapter();
89
- const controller = new AbortController();
90
- controller.abort();
91
-
92
- await expect(
93
- adapter.waitForAnalytic('button-click', { signal: controller.signal }),
94
- ).rejects.toThrow();
95
- });
96
-
97
- it('rejects pending waitForAnalytic calls when the adapter is disposed', async () => {
98
- const adapter = new MockAnalyticsAdapter();
99
-
100
- const promise = adapter.waitForAnalytic('button-click');
101
- adapter[Symbol.dispose]();
102
-
103
- await expect(promise).rejects.toThrow('disposed before a matching event was recorded');
104
- });
105
-
106
- it('rejects instead of hanging when a predicate matcher throws on a future event', async () => {
107
- const adapter = new MockAnalyticsAdapter();
108
- const boom = new Error('predicate boom');
109
-
110
- const promise = adapter.waitForAnalytic(() => {
111
- throw boom;
112
- });
113
- adapter.registerAnalytic(createEvent('button-click'));
114
-
115
- await expect(promise).rejects.toThrow(boom);
116
- });
117
-
118
- it('does not interfere with events recorded by another adapter instance', () => {
119
- const adapterA = new MockAnalyticsAdapter();
120
- const adapterB = new MockAnalyticsAdapter();
121
-
122
- adapterA.registerAnalytic(createEvent('button-click'));
123
-
124
- expect(adapterA.getAnalytics()).toHaveLength(1);
125
- expect(adapterB.getAnalytics()).toHaveLength(0);
126
- });
127
- });