@equinor/fusion-framework-module-analytics 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +248 -0
  4. package/dist/esm/AnalyticsConfigurator.interface.js +2 -0
  5. package/dist/esm/AnalyticsConfigurator.interface.js.map +1 -0
  6. package/dist/esm/AnalyticsConfigurator.js +61 -0
  7. package/dist/esm/AnalyticsConfigurator.js.map +1 -0
  8. package/dist/esm/AnalyticsProvider.interface.js +2 -0
  9. package/dist/esm/AnalyticsProvider.interface.js.map +1 -0
  10. package/dist/esm/AnalyticsProvider.js +92 -0
  11. package/dist/esm/AnalyticsProvider.js.map +1 -0
  12. package/dist/esm/adapters/AnalyticsAdapter.interface.js +2 -0
  13. package/dist/esm/adapters/AnalyticsAdapter.interface.js.map +1 -0
  14. package/dist/esm/adapters/ConsoleAnalyticsAdapter.js +12 -0
  15. package/dist/esm/adapters/ConsoleAnalyticsAdapter.js.map +1 -0
  16. package/dist/esm/adapters/FusionAnalyticsAdapter.js +42 -0
  17. package/dist/esm/adapters/FusionAnalyticsAdapter.js.map +1 -0
  18. package/dist/esm/adapters/index.js +3 -0
  19. package/dist/esm/adapters/index.js.map +1 -0
  20. package/dist/esm/collectors/AnalyticsCollector.interface.js +2 -0
  21. package/dist/esm/collectors/AnalyticsCollector.interface.js.map +1 -0
  22. package/dist/esm/collectors/AppLoadedCollector.js +42 -0
  23. package/dist/esm/collectors/AppLoadedCollector.js.map +1 -0
  24. package/dist/esm/collectors/AppSelectedCollector.js +32 -0
  25. package/dist/esm/collectors/AppSelectedCollector.js.map +1 -0
  26. package/dist/esm/collectors/BaseCollector.js +40 -0
  27. package/dist/esm/collectors/BaseCollector.js.map +1 -0
  28. package/dist/esm/collectors/ContextSelectedCollector.js +36 -0
  29. package/dist/esm/collectors/ContextSelectedCollector.js.map +1 -0
  30. package/dist/esm/collectors/index.js +4 -0
  31. package/dist/esm/collectors/index.js.map +1 -0
  32. package/dist/esm/collectors/utils/extractAppMetadata.js +37 -0
  33. package/dist/esm/collectors/utils/extractAppMetadata.js.map +1 -0
  34. package/dist/esm/collectors/utils/extractContextMetadata.js +23 -0
  35. package/dist/esm/collectors/utils/extractContextMetadata.js.map +1 -0
  36. package/dist/esm/enable-analytics.js +20 -0
  37. package/dist/esm/enable-analytics.js.map +1 -0
  38. package/dist/esm/index.js +5 -0
  39. package/dist/esm/index.js.map +1 -0
  40. package/dist/esm/logExporters/fusionOTLPLogExporter/FusionOTLPLogExporter.js +22 -0
  41. package/dist/esm/logExporters/fusionOTLPLogExporter/FusionOTLPLogExporter.js.map +1 -0
  42. package/dist/esm/logExporters/fusionOTLPLogExporter/HttpClientExporterTransport.js +84 -0
  43. package/dist/esm/logExporters/fusionOTLPLogExporter/HttpClientExporterTransport.js.map +1 -0
  44. package/dist/esm/logExporters/index.js +3 -0
  45. package/dist/esm/logExporters/index.js.map +1 -0
  46. package/dist/esm/module.js +28 -0
  47. package/dist/esm/module.js.map +1 -0
  48. package/dist/esm/types.js +2 -0
  49. package/dist/esm/types.js.map +1 -0
  50. package/dist/esm/version.js +3 -0
  51. package/dist/esm/version.js.map +1 -0
  52. package/dist/tsconfig.tsbuildinfo +1 -0
  53. package/dist/types/AnalyticsConfigurator.d.ts +39 -0
  54. package/dist/types/AnalyticsConfigurator.interface.d.ts +39 -0
  55. package/dist/types/AnalyticsProvider.d.ts +45 -0
  56. package/dist/types/AnalyticsProvider.interface.d.ts +20 -0
  57. package/dist/types/adapters/AnalyticsAdapter.interface.d.ts +21 -0
  58. package/dist/types/adapters/ConsoleAnalyticsAdapter.d.ts +9 -0
  59. package/dist/types/adapters/FusionAnalyticsAdapter.d.ts +19 -0
  60. package/dist/types/adapters/index.d.ts +3 -0
  61. package/dist/types/collectors/AnalyticsCollector.interface.d.ts +16 -0
  62. package/dist/types/collectors/AppLoadedCollector.d.ts +23 -0
  63. package/dist/types/collectors/AppSelectedCollector.d.ts +21 -0
  64. package/dist/types/collectors/BaseCollector.d.ts +27 -0
  65. package/dist/types/collectors/ContextSelectedCollector.d.ts +21 -0
  66. package/dist/types/collectors/index.d.ts +4 -0
  67. package/dist/types/collectors/utils/extractAppMetadata.d.ts +17 -0
  68. package/dist/types/collectors/utils/extractContextMetadata.d.ts +11 -0
  69. package/dist/types/enable-analytics.d.ts +11 -0
  70. package/dist/types/index.d.ts +7 -0
  71. package/dist/types/logExporters/fusionOTLPLogExporter/FusionOTLPLogExporter.d.ts +11 -0
  72. package/dist/types/logExporters/fusionOTLPLogExporter/HttpClientExporterTransport.d.ts +12 -0
  73. package/dist/types/logExporters/index.d.ts +2 -0
  74. package/dist/types/module.d.ts +44 -0
  75. package/dist/types/types.d.ts +22 -0
  76. package/dist/types/version.d.ts +1 -0
  77. package/package.json +72 -0
  78. package/src/AnalyticsConfigurator.interface.ts +48 -0
  79. package/src/AnalyticsConfigurator.ts +116 -0
  80. package/src/AnalyticsProvider.interface.ts +22 -0
  81. package/src/AnalyticsProvider.ts +118 -0
  82. package/src/adapters/AnalyticsAdapter.interface.ts +23 -0
  83. package/src/adapters/ConsoleAnalyticsAdapter.ts +17 -0
  84. package/src/adapters/FusionAnalyticsAdapter.ts +61 -0
  85. package/src/adapters/index.ts +3 -0
  86. package/src/collectors/AnalyticsCollector.interface.ts +18 -0
  87. package/src/collectors/AppLoadedCollector.ts +74 -0
  88. package/src/collectors/AppSelectedCollector.ts +53 -0
  89. package/src/collectors/BaseCollector.ts +65 -0
  90. package/src/collectors/ContextSelectedCollector.ts +56 -0
  91. package/src/collectors/index.ts +4 -0
  92. package/src/collectors/utils/extractAppMetadata.ts +45 -0
  93. package/src/collectors/utils/extractContextMetadata.ts +27 -0
  94. package/src/enable-analytics.ts +27 -0
  95. package/src/index.ts +11 -0
  96. package/src/logExporters/fusionOTLPLogExporter/FusionOTLPLogExporter.ts +41 -0
  97. package/src/logExporters/fusionOTLPLogExporter/HttpClientExporterTransport.ts +89 -0
  98. package/src/logExporters/index.ts +2 -0
  99. package/src/module.ts +59 -0
  100. package/src/types.ts +29 -0
  101. package/src/version.ts +2 -0
  102. package/tsconfig.json +29 -0
@@ -0,0 +1,116 @@
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
+ * Configures analytics settings for the module.
16
+ *
17
+ * The `AnalyticsConfigurator` class extends `BaseConfigBuilder` to provide a fluent API for
18
+ * setting up analytics adapters and collectors.
19
+ *
20
+ * @remarks
21
+ * - Adapters are managed internally and can be set using `setAdapter`.
22
+ * - Collectors are managed internally and can be set using `setCollector`.
23
+ * - All setter methods return `this` for method chaining.
24
+ *
25
+ * @see BaseConfigBuilder
26
+ * @see IAnalyticsConfigurator
27
+ */
28
+ export class AnalyticsConfigurator
29
+ extends BaseConfigBuilder<AnalyticsConfig>
30
+ implements IAnalyticsConfigurator
31
+ {
32
+ #collectorCallbacks: Record<string, ConfigBuilderCallback<IAnalyticsCollector>> = {};
33
+ #adapterCallbacks: Record<string, ConfigBuilderCallback<IAnalyticsAdapter>> = {};
34
+
35
+ constructor() {
36
+ super();
37
+
38
+ // Configure async collectors resolution using mergeMap to handle Promise/Observable collector factories
39
+ this._set(
40
+ 'collectors',
41
+ (args: ConfigBuilderCallbackArgs): ObservableInput<Record<string, IAnalyticsCollector>> => {
42
+ return from(Object.entries(this.#collectorCallbacks)).pipe(
43
+ mergeMap(([identifier, collectorFn]) =>
44
+ from(collectorFn(args)).pipe(
45
+ filter((collector): collector is IAnalyticsCollector => !!collector),
46
+ map((collector) => [identifier, collector] as const),
47
+ ),
48
+ ),
49
+ scan(
50
+ (acc, [identifier, collector]) => {
51
+ acc[identifier] = collector;
52
+ return acc;
53
+ },
54
+ {} as Record<string, IAnalyticsCollector>,
55
+ ),
56
+ defaultIfEmpty({}),
57
+ shareReplay({ bufferSize: 1, refCount: true }),
58
+ );
59
+ },
60
+ );
61
+
62
+ // Configure async adapters resolution using mergeMap to handle Promise/Observable adapter factories
63
+ this._set(
64
+ 'adapters',
65
+ (args: ConfigBuilderCallbackArgs): ObservableInput<Record<string, IAnalyticsAdapter>> => {
66
+ return from(Object.entries(this.#adapterCallbacks)).pipe(
67
+ mergeMap(([identifier, adapterFn]) =>
68
+ from(adapterFn(args)).pipe(
69
+ filter((adapter): adapter is IAnalyticsAdapter => !!adapter),
70
+ map((adapter) => [identifier, adapter] as const),
71
+ ),
72
+ ),
73
+ scan(
74
+ (acc, [identifier, adapter]) => {
75
+ acc[identifier] = adapter;
76
+ return acc;
77
+ },
78
+ {} as Record<string, IAnalyticsAdapter>,
79
+ ),
80
+ defaultIfEmpty({}),
81
+ shareReplay({ bufferSize: 1, refCount: true }),
82
+ );
83
+ },
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Registers a analytics collector with the configurator.
89
+ *
90
+ * @param identifier - The name of the collector
91
+ * @param callback - A callback function that returns an analytics collector instance
92
+ * @returns The current instance for method chaining
93
+ */
94
+ setCollector<T extends AnalyticsEvent>(
95
+ identifier: string,
96
+ callback: ConfigBuilderCallback<IAnalyticsCollector<T>>,
97
+ ): this {
98
+ this.#collectorCallbacks[identifier] = callback;
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Registers a analytics adapter with the configurator.
104
+ *
105
+ * @param identifier - The name of the adapter
106
+ * @param callback - A callback function that returns an analytics adapter instance
107
+ * @returns The current instance for method chaining
108
+ */
109
+ setAdapter<T extends AnalyticsEvent>(
110
+ identifier: string,
111
+ callback: ConfigBuilderCallback<IAnalyticsAdapter<T>>,
112
+ ): this {
113
+ this.#adapterCallbacks[identifier] = callback;
114
+ return this;
115
+ }
116
+ }
@@ -0,0 +1,22 @@
1
+ import type { ObservableInput, Subscription } from 'rxjs';
2
+ import type { AnalyticsEvent } from './types.js';
3
+
4
+ /**
5
+ * Interface for analytics providers used to track analytics events.
6
+ */
7
+ export interface IAnalyticsProvider {
8
+ /**
9
+ * Tracks a analytics event.
10
+ *
11
+ * @param event - The analytic event to track.
12
+ */
13
+ trackAnalytic(event: AnalyticsEvent): void;
14
+
15
+ /**
16
+ * Uses a analytics stream and returns both Disposable and Subscription for cleanup.
17
+ *
18
+ * @param analytic$ - Observable input stream of analytic events.
19
+ * @returns Object containing both Disposable and Subscription for proper cleanup.
20
+ */
21
+ trackAnalytic$(analytic$: ObservableInput<AnalyticsEvent>): Disposable & Subscription;
22
+ }
@@ -0,0 +1,118 @@
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
+ class DisposableSubscription extends Subscription {
13
+ constructor(subscription: Subscription) {
14
+ super(subscription.unsubscribe);
15
+ }
16
+
17
+ [Symbol.dispose] = () => {
18
+ this.unsubscribe();
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Provides analytics tracking, adapters integration and collectors for application instrumentation.
24
+ *
25
+ * The `AnalyticsProvider` class is responsible for collecting, processing and relaying analytics
26
+ * data to the adapters. The events are collected with collectors.
27
+ *
28
+ * @typeParam AnalyticsConfig - The configuration type for analytics.
29
+ * @implements IAnalyticsProvider
30
+ * @extends BaseModuleProvider<AnalyticsConfig>
31
+ */
32
+ export class AnalyticsProvider
33
+ extends BaseModuleProvider<AnalyticsConfig>
34
+ implements IAnalyticsProvider
35
+ {
36
+ #analytics: Subject<AnalyticsEvent>;
37
+ #collectors: Record<string, IAnalyticsCollector>;
38
+ #adapters: Record<string, IAnalyticsAdapter>;
39
+
40
+ constructor(config: AnalyticsConfig) {
41
+ super({ version, config });
42
+
43
+ this.#analytics = new Subject();
44
+ this.#collectors = config.collectors;
45
+ this.#adapters = config.adapters;
46
+ }
47
+
48
+ /**
49
+ * Initializes the analytics provider with adapters and collectors.
50
+ *
51
+ * This method sets up the provider for operation by:
52
+ * 1. Storing the provided adapters
53
+ * 2. Initializing all adapters
54
+ * 3. Storing the provided collectors
55
+ * 4. Initializing all collectors
56
+ * 5. Setting up subscription for analytics processing
57
+ *
58
+ * @returns A promise that resolves when initialization is complete
59
+ */
60
+ async initialize(): Promise<void> {
61
+ const initializedCollectors = Object.values(this.#collectors).map((collector) =>
62
+ Promise.resolve(collector.initialize?.()),
63
+ );
64
+ const initializedAdapters = Object.values(this.#adapters).map((adapters) =>
65
+ Promise.resolve(adapters.initialize?.()),
66
+ );
67
+
68
+ await Promise.allSettled(initializedCollectors);
69
+ await Promise.allSettled(initializedAdapters);
70
+
71
+ for (const collector of Object.values(this.#collectors)) {
72
+ const subscription = collector.subscribe({
73
+ next: (event) => {
74
+ this.#analytics.next(event);
75
+ },
76
+ });
77
+
78
+ this._addTeardown(subscription);
79
+ }
80
+
81
+ const adapterSubscription = this.#analytics.subscribe({
82
+ next: (event) => {
83
+ for (const adapter of Object.values(this.#adapters)) {
84
+ adapter.registerAnalytic(event);
85
+ }
86
+ },
87
+ });
88
+ this._addTeardown(adapterSubscription);
89
+ }
90
+
91
+ /**
92
+ * Tracks an analytics event
93
+ *
94
+ * @param event - The analytics event to track
95
+ */
96
+ trackAnalytic(event: AnalyticsEvent): void {
97
+ // @TODO: Validate AnalyticsEvent includes name, value and attributes
98
+ this.#analytics.next(event);
99
+ }
100
+
101
+ /**
102
+ * Uses a analytics stream and returns both Disposable and Subscription for cleanup.
103
+ *
104
+ * @param analytic$ - Observable input stream of analytic events.
105
+ * @returns Object containing both Disposable and Subscription for proper cleanup.
106
+ */
107
+ trackAnalytic$(analytic$: ObservableInput<AnalyticsEvent>): Disposable & Subscription {
108
+ const subscription = from(analytic$)
109
+ // @TODO: Validate AnalyticsEvent includes name, value and attributes
110
+ .subscribe({
111
+ next: (event) => {
112
+ this.#analytics.next(event);
113
+ },
114
+ });
115
+
116
+ return new DisposableSubscription(subscription);
117
+ }
118
+ }
@@ -0,0 +1,23 @@
1
+ import type { AnalyticsEvent } from '../types.js';
2
+
3
+ /**
4
+ * Interface representing an analytics adapter responsible for handling analytics events.
5
+ *
6
+ * @template T - The type of analytics event handled by the adapter. Defaults to `AnalyticsEvent`.
7
+ */
8
+ export interface IAnalyticsAdapter<T extends AnalyticsEvent = AnalyticsEvent> extends Disposable {
9
+ /**
10
+ * Initializes the analytics adapter for setup and configuration.
11
+ *
12
+ * @returns Promise that resolves when initialization is complete, or void for synchronous initialization.
13
+ */
14
+ initialize?(): Promise<void> | void;
15
+
16
+ /**
17
+ * Exports analytics event to the configured backend.
18
+ *
19
+ * @param events - Array of analytics events to export.
20
+ * @returns Promise that resolves when export is complete.
21
+ */
22
+ registerAnalytic(event: T): Promise<void> | void;
23
+ }
@@ -0,0 +1,17 @@
1
+ import type { IAnalyticsAdapter } from './AnalyticsAdapter.interface.js';
2
+ import type { AnalyticsEvent } from '../types.js';
3
+
4
+ /**
5
+ * An analytics adapter printing the events to the console.
6
+ */
7
+ export class ConsoleAnalyticsAdapter<T extends AnalyticsEvent = AnalyticsEvent>
8
+ implements IAnalyticsAdapter
9
+ {
10
+ registerAnalytic(event: T): Promise<void> | void {
11
+ console.log('Analytics::Adapter::Console', event);
12
+ }
13
+
14
+ [Symbol.dispose]() {
15
+ // no-op
16
+ }
17
+ }
@@ -0,0 +1,61 @@
1
+ import type { IAnalyticsAdapter } from './AnalyticsAdapter.interface.js';
2
+ import type { AnalyticsEvent } from '../types.js';
3
+
4
+ import {
5
+ LoggerProvider,
6
+ SimpleLogRecordProcessor,
7
+ type ReadableLogRecord,
8
+ } from '@opentelemetry/sdk-logs';
9
+ import type { OTLPExporterBase } from '@opentelemetry/otlp-exporter-base';
10
+ import {
11
+ SeverityNumber,
12
+ type LogAttributes,
13
+ type Logger,
14
+ type LogRecord,
15
+ } from '@opentelemetry/api-logs';
16
+ import { resourceFromAttributes } from '@opentelemetry/resources';
17
+ import { version } from '../version.js';
18
+ import { v7 as uuid } from 'uuid';
19
+
20
+ /**
21
+ * An analytics adapter for sending events to the log exporter for handling.
22
+ *
23
+ * The exporter is a Open Telemetry exporter.
24
+ * @see OTLPExporterBase
25
+ */
26
+ export class FusionAnalyticsAdapter<T extends AnalyticsEvent = AnalyticsEvent>
27
+ implements IAnalyticsAdapter
28
+ {
29
+ #logExporter: OTLPExporterBase<ReadableLogRecord[]>;
30
+ #loggerProvider: LoggerProvider;
31
+ #logger: Logger;
32
+
33
+ constructor(args: { portalId: string; logExporter: OTLPExporterBase<ReadableLogRecord[]> }) {
34
+ this.#logExporter = args.logExporter;
35
+
36
+ this.#loggerProvider = new LoggerProvider({
37
+ processors: [new SimpleLogRecordProcessor(this.#logExporter)],
38
+ resource: resourceFromAttributes({
39
+ 'module.version': version,
40
+ 'session.id': uuid(),
41
+ 'portal.id': args.portalId,
42
+ }),
43
+ });
44
+ this.#logger = this.#loggerProvider.getLogger('fusion');
45
+ }
46
+
47
+ registerAnalytic(event: T): Promise<void> | void {
48
+ const logRecord: Partial<LogRecord> = {
49
+ eventName: event.name,
50
+ attributes: event.attributes as LogAttributes,
51
+ body: event.value,
52
+ severityNumber: SeverityNumber.INFO,
53
+ };
54
+ this.#logger.emit(logRecord);
55
+ }
56
+
57
+ [Symbol.dispose]() {
58
+ this.#logExporter.shutdown();
59
+ this.#loggerProvider.shutdown();
60
+ }
61
+ }
@@ -0,0 +1,3 @@
1
+ export { IAnalyticsAdapter } from './AnalyticsAdapter.interface.js';
2
+ export { ConsoleAnalyticsAdapter } from './ConsoleAnalyticsAdapter.js';
3
+ export { FusionAnalyticsAdapter } from './FusionAnalyticsAdapter.js';
@@ -0,0 +1,18 @@
1
+ import type { Subscribable } from 'rxjs';
2
+ import type { AnalyticsEvent } from '../types.js';
3
+
4
+ /**
5
+ * Interface representing an analytics collector responsible for sending analytics events.
6
+ *
7
+ * @template T - The type of analytics event handled by the adapter. Defaults to `AnalyticsEvent`.
8
+ */
9
+ export interface IAnalyticsCollector<T extends AnalyticsEvent = AnalyticsEvent>
10
+ extends Subscribable<T> {
11
+ /**
12
+ * Initializes the analytics collector for setup and configuration.
13
+ * This method is optional - if not implemented, the collector should be directly subscribable.
14
+ *
15
+ * @returns Promise that resolves when initialization is complete, or void for synchronous initialization.
16
+ */
17
+ initialize?(): Promise<void> | void;
18
+ }
@@ -0,0 +1,74 @@
1
+ import { BaseCollector, createSchema } from './BaseCollector.js';
2
+ import { type AppItemType, appSchema, extractAppMetadata } from './utils/extractAppMetadata.js';
3
+ import {
4
+ type ContextItemType,
5
+ contextSchema,
6
+ extractContextMetadata,
7
+ } from './utils/extractContextMetadata.js';
8
+ import type { IAnalyticsCollector } from './AnalyticsCollector.interface.js';
9
+
10
+ import type {
11
+ AppModulesInstance,
12
+ AppManifest,
13
+ AppModuleProvider,
14
+ } from '@equinor/fusion-framework-module-app';
15
+ import type { IEventModuleProvider } from '@equinor/fusion-framework-module-event';
16
+
17
+ import { type ObservableInput, Subject } from 'rxjs';
18
+ import { z } from 'zod';
19
+ import type { ContextModule } from '@equinor/fusion-framework-module-context';
20
+
21
+ const EVENT_NAME = 'onAppModulesLoaded';
22
+
23
+ /**
24
+ * The schema of the data to be sent.
25
+ */
26
+ const eventSchema = createSchema(appSchema, z.object({ context: contextSchema }));
27
+
28
+ /**
29
+ * Collector to listen for app loaded and add values and attributes for
30
+ * further processing by adapters.
31
+ */
32
+ export class AppLoadedCollector
33
+ extends BaseCollector<AppItemType, { context?: ContextItemType }>
34
+ implements IAnalyticsCollector
35
+ {
36
+ #eventProvider: IEventModuleProvider;
37
+ #appProvider: AppModuleProvider;
38
+
39
+ constructor(eventProvider: IEventModuleProvider, appProvider: AppModuleProvider) {
40
+ super('app-loaded', eventSchema);
41
+ this.#eventProvider = eventProvider;
42
+ this.#appProvider = appProvider;
43
+ }
44
+
45
+ _initialize(): ObservableInput<{
46
+ value: AppItemType;
47
+ attributes: { context?: ContextItemType };
48
+ }> {
49
+ const subject = new Subject<{
50
+ value: AppItemType;
51
+ attributes: { context?: ContextItemType };
52
+ }>();
53
+ this.#eventProvider.addEventListener(EVENT_NAME, (event) => {
54
+ // Fallback to appProvider for manifest if app is not updated with latest
55
+ // payload for onAppModulesLoaded (missing manifest).
56
+ const manifest =
57
+ (event.detail.manifest as AppManifest) ?? this.#appProvider.current?.manifest;
58
+ const modules = event.detail.modules as AppModulesInstance<[ContextModule]>;
59
+
60
+ const data = {
61
+ value: manifest && extractAppMetadata(manifest),
62
+ attributes: {
63
+ context:
64
+ modules.context?.currentContext &&
65
+ extractContextMetadata(modules.context.currentContext),
66
+ },
67
+ };
68
+
69
+ subject.next(data);
70
+ });
71
+
72
+ return subject;
73
+ }
74
+ }
@@ -0,0 +1,53 @@
1
+ import { map, type ObservableInput, pairwise } from 'rxjs';
2
+ import type { IAnalyticsCollector } from './AnalyticsCollector.interface.js';
3
+ import { BaseCollector, createSchema } from './BaseCollector.js';
4
+ import {
5
+ type AppKeyType,
6
+ appKeySchema,
7
+ extractAppKeyMetadata,
8
+ } from './utils/extractAppMetadata.js';
9
+
10
+ import type { AppModuleProvider } from '@equinor/fusion-framework-module-app';
11
+
12
+ import { z } from 'zod';
13
+
14
+ /**
15
+ * The schema of the data to be sent.
16
+ */
17
+ const eventSchema = createSchema(appKeySchema, z.object({ previous: appKeySchema }));
18
+
19
+ /**
20
+ * Collector to listen for app selection and add values and attributes for
21
+ * further processing by adapters.
22
+ */
23
+ export class AppSelectedCollector
24
+ extends BaseCollector<AppKeyType, { previous?: AppKeyType }>
25
+ implements IAnalyticsCollector
26
+ {
27
+ #appProvider: AppModuleProvider;
28
+
29
+ constructor(appProvider: AppModuleProvider) {
30
+ super('app-selected', eventSchema);
31
+ this.#appProvider = appProvider;
32
+ }
33
+
34
+ _initialize(): ObservableInput<{
35
+ value: AppKeyType;
36
+ attributes: { previous?: AppKeyType };
37
+ }> {
38
+ const appSelected$ = this.#appProvider.current$.pipe(pairwise());
39
+
40
+ const data$ = appSelected$.pipe(
41
+ map(([prev, next]) => {
42
+ return {
43
+ value: extractAppKeyMetadata(next),
44
+ attributes: {
45
+ previous: prev && extractAppKeyMetadata(prev),
46
+ },
47
+ };
48
+ }),
49
+ );
50
+
51
+ return data$;
52
+ }
53
+ }
@@ -0,0 +1,65 @@
1
+ import type { AnyValue, AnyValueMap, AnalyticsEvent } from '../types.js';
2
+ import type { IAnalyticsCollector } from './AnalyticsCollector.interface.js';
3
+
4
+ import { from, map, type ObservableInput, Subject, type Observer, type Unsubscribable } from 'rxjs';
5
+ import { z } from 'zod';
6
+
7
+ /**
8
+ * Function to create zod schema to be used when collecting
9
+ */
10
+ export const createSchema = <TValue = AnyValue, TAttr = AnyValueMap>(
11
+ value: z.ZodSchema<TValue>,
12
+ attributes: z.ZodSchema<TAttr>,
13
+ ) => {
14
+ return z.object({
15
+ name: z.string(),
16
+ value: value,
17
+ attributes: attributes.optional(),
18
+ });
19
+ };
20
+
21
+ export type CollectorSchema<TValue = AnyValue, TAttr = AnyValueMap> = ReturnType<
22
+ typeof createSchema<TValue, TAttr>
23
+ >;
24
+
25
+ /**
26
+ * A base collector to help with parsing the events with provided schema before
27
+ * emitting.
28
+ */
29
+ export abstract class BaseCollector<
30
+ TValue extends AnyValue,
31
+ TAttr extends AnyValueMap = AnyValueMap,
32
+ > implements IAnalyticsCollector<AnalyticsEvent<TValue, TAttr>>
33
+ {
34
+ #schema: CollectorSchema<TValue, TAttr>;
35
+ #name: string;
36
+ #subject: Subject<z.infer<CollectorSchema<TValue, TAttr>>> = new Subject<
37
+ z.infer<CollectorSchema<TValue, TAttr>>
38
+ >();
39
+
40
+ constructor(name: string, schema: CollectorSchema<TValue, TAttr>) {
41
+ this.#name = name;
42
+ this.#schema = schema;
43
+ }
44
+
45
+ abstract _initialize(): ObservableInput<{ value: TValue; attributes: TAttr }>;
46
+
47
+ initialize(): Promise<void> | void {
48
+ from(this._initialize())
49
+ .pipe(
50
+ map(({ value, attributes }) => {
51
+ return {
52
+ name: this.#name,
53
+ value: value,
54
+ attributes: attributes,
55
+ };
56
+ }),
57
+ map((x) => this.#schema.parse(x)),
58
+ )
59
+ .subscribe(this.#subject);
60
+ }
61
+
62
+ subscribe(observer: Partial<Observer<z.infer<CollectorSchema<TValue, TAttr>>>>): Unsubscribable {
63
+ return this.#subject.subscribe(observer);
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ import { distinctUntilChanged, map, type ObservableInput, pairwise } from 'rxjs';
2
+ import type { IAnalyticsCollector } from './AnalyticsCollector.interface.js';
3
+ import type { IContextProvider } from '@equinor/fusion-framework-module-context';
4
+ import { z } from 'zod';
5
+ import { BaseCollector, createSchema } from './BaseCollector.js';
6
+ import {
7
+ type ContextItemType,
8
+ contextSchema,
9
+ extractContextMetadata,
10
+ } from './utils/extractContextMetadata.js';
11
+
12
+ /**
13
+ * The schema of the data to be sent.
14
+ */
15
+ const eventSchema = createSchema(contextSchema, z.object({ previous: contextSchema }));
16
+
17
+ /**
18
+ * Collector to listen for context changes and add values and attributes for
19
+ * further processing by adapters.
20
+ */
21
+ export class ContextSelectedCollector
22
+ extends BaseCollector<ContextItemType, { previous?: ContextItemType }>
23
+ implements IAnalyticsCollector
24
+ {
25
+ #contextProvider: IContextProvider;
26
+
27
+ constructor(contextProvider: IContextProvider) {
28
+ super('context-selected', eventSchema);
29
+ this.#contextProvider = contextProvider;
30
+ }
31
+
32
+ _initialize(): ObservableInput<{
33
+ value: ContextItemType;
34
+ attributes: { previous?: ContextItemType };
35
+ }> {
36
+ const contextSelected$ = this.#contextProvider.currentContext$.pipe(
37
+ // Only emit when an actual change has happened.
38
+ distinctUntilChanged((prev, curr) => prev?.id === curr?.id),
39
+ // Provide both the old and the new value.
40
+ pairwise(),
41
+ );
42
+
43
+ const data$ = contextSelected$.pipe(
44
+ map(([prev, next]) => {
45
+ return {
46
+ value: next && extractContextMetadata(next),
47
+ attributes: {
48
+ previous: prev && extractContextMetadata(prev),
49
+ },
50
+ };
51
+ }),
52
+ );
53
+
54
+ return data$;
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ export { IAnalyticsCollector } from './AnalyticsCollector.interface.js';
2
+ export { ContextSelectedCollector } from './ContextSelectedCollector.js';
3
+ export { AppSelectedCollector } from './AppSelectedCollector.js';
4
+ export { AppLoadedCollector } from './AppLoadedCollector.js';
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ import type { AppManifest, CurrentApp } from '@equinor/fusion-framework-module-app';
3
+
4
+ // Schema representing an object with key appKey and a string value.
5
+ // Used to parse an object containing appKey.
6
+ export const appKeySchema = z
7
+ .object({
8
+ appKey: z.string().optional(),
9
+ })
10
+ .optional()
11
+ .nullable();
12
+
13
+ // Schema representing an object with data points of an app.
14
+ // Used to parse an object with app data.
15
+ export const appSchema = z
16
+ .object({
17
+ appKey: z.string(),
18
+ displayName: z.string(),
19
+ type: z.string(),
20
+ categoryName: z.string().optional(),
21
+ buildVersion: z.string().optional(),
22
+ buildTag: z.string().optional().nullable(),
23
+ })
24
+ .optional();
25
+
26
+ export type AppKeyType = z.infer<typeof appKeySchema>;
27
+
28
+ export type AppItemType = z.infer<typeof appSchema>;
29
+
30
+ export const extractAppKeyMetadata = (app: CurrentApp): z.input<typeof appKeySchema> => {
31
+ return {
32
+ appKey: app?.appKey,
33
+ };
34
+ };
35
+
36
+ export const extractAppMetadata = (app: AppManifest): z.input<typeof appSchema> => {
37
+ return {
38
+ appKey: app.appKey,
39
+ displayName: app.displayName,
40
+ type: app.type,
41
+ categoryName: app.category?.name,
42
+ buildVersion: app.build?.version,
43
+ buildTag: app.build?.tag,
44
+ };
45
+ };