Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/docs_analytics-docs-restructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@equinor/fusion-framework-docs": patch
---

Add the analytics module's `docs/` pages (`adapters.md`, `collectors.md`, `tracking-events.md`,
`testing.md`) to the vue-press site, matching the `event`/`http`/`module` `docs/` convention:
`README.md` is slimmed to an overview, entry points, and a documentation table, and the new
`analytics/docs/*.md` pages `@include` the package's own docs instead of duplicating content.
The sidebar is updated to match.
28 changes: 28 additions & 0 deletions .changeset/module-analytics_mock-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@equinor/fusion-framework-module-analytics": minor
---

Add `MockAnalyticsAdapter` and `./mock` subpath for asserting on tracked analytics events in tests.

```ts
import { enableAnalytics } from '@equinor/fusion-framework-module-analytics';
import { MockAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/mock';

const recorder = new MockAnalyticsAdapter();

enableAnalytics(configurator, (builder) => {
builder.setAdapter('mock', async () => recorder);
});

// ... exercise the app under test, then assert:
const event = await recorder.waitForAnalytic('button-click');
expect(event.attributes?.section).toBe('header');
```

### `getAnalytics(matcher?)`

Returns recorded events synchronously, filtered by an event name, an array of names, or a predicate. Omit the matcher to get every recorded event.

### `waitForAnalytic(matcher, options?)`

Resolves with the first matching event, resolving immediately if one was already recorded, or waiting for a future one. Supports an optional `timeout` (ms) and `AbortSignal` so a test cannot hang indefinitely, and rejects if the adapter is disposed before a match occurs.
236 changes: 22 additions & 214 deletions packages/modules/analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
Fusion Framework module for collecting and exporting application analytics using
OpenTelemetry standards.

## Who should use this

- **Application and portal developers** who want to track user interactions
(clicks, context changes, app usage) without wiring up telemetry by hand.
- **Module authors** who want their module's lifecycle events picked up by
analytics automatically via a collector.
- **Test authors** who need to assert which analytics events an app or
collector produced.

## Overview

The analytics module provides a pluggable **adapter/collector** architecture:
Expand All @@ -22,6 +31,16 @@ When a collector emits an event it is delivered to **every** registered adapter.
| `@equinor/fusion-framework-module-analytics/adapters` | `ConsoleAnalyticsAdapter`, `FusionAnalyticsAdapter`, `IAnalyticsAdapter` |
| `@equinor/fusion-framework-module-analytics/collectors` | `ContextSelectedCollector`, `AppSelectedCollector`, `AppLoadedCollector`, `IAnalyticsCollector` |
| `@equinor/fusion-framework-module-analytics/logExporters` | `OTLPLogExporter`, `FusionOTLPLogExporter` |
| `@equinor/fusion-framework-module-analytics/mock` | `MockAnalyticsAdapter` — record tracked events for test assertions |

## Documentation

| Topic | Description |
|---|---|
| [Adapters](docs/adapters.md) | `ConsoleAnalyticsAdapter`, `FusionAnalyticsAdapter`, and creating a custom `IAnalyticsAdapter` |
| [Collectors](docs/collectors.md) | Built-in collectors (context/app selection, app loaded) and creating a custom `IAnalyticsCollector` |
| [Tracking Events Manually](docs/tracking-events.md) | `provider.trackAnalytic` / `trackAnalytic$` for ad-hoc event tracking |
| [Testing](docs/testing.md) | `MockAnalyticsAdapter`, recording and awaiting tracked events, and using a bespoke `ModulesConfigurator` in tests |

## Quick Start

Expand Down Expand Up @@ -52,217 +71,6 @@ const configure = (configurator) => {
> Fusion Framework module system. Manual initialisation is only required when
> accessing the provider directly.

## Adapters

Adapters implement `IAnalyticsAdapter` and are responsible for processing and
sending analytics data to their destinations. All adapters support async
initialisation and will be initialised automatically when the provider starts.

### ConsoleAnalyticsAdapter

Logs every analytics event to the browser console. Useful for development and
debugging. No configuration required.

```typescript
builder.setAdapter('console', async () => new ConsoleAnalyticsAdapter());
```

### FusionAnalyticsAdapter

Forwards analytics events to an OpenTelemetry-compatible log endpoint via a
bundled `LoggerProvider`.

Configuration options:

| Option | Type | Description |
|---|---|---|
| `portalId` | `string` | Portal identifier included in every log record |
| `logExporter` | `OTLPExporterBase` | OTLP log exporter for transport |

#### Using `OTLPLogExporter` (direct HTTP)

```typescript
import { OTLPLogExporter } from '@equinor/fusion-framework-module-analytics/logExporters';
import { FusionAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';

builder.setAdapter('fusion-log', async () => {
const logExporter = new OTLPLogExporter({
url: 'https://example.com/v1/logs',
headers: { 'Content-Type': 'application/json' },
});
return new FusionAnalyticsAdapter({ portalId: 'my-portal', logExporter });
});
```

#### Using `FusionOTLPLogExporter` (service discovery HTTP client)

```typescript
import { FusionOTLPLogExporter } from '@equinor/fusion-framework-module-analytics/logExporters';
import { FusionAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';

builder.setAdapter('fusion', async (args) => {
if (args.hasModule('serviceDiscovery')) {
const sd = await args.requireInstance('serviceDiscovery');
const httpClient = await sd.createClient('analytics');
const logExporter = new FusionOTLPLogExporter(httpClient);
return new FusionAnalyticsAdapter({ portalId: 'my-portal', logExporter });
}
console.error('Service discovery unavailable — analytics adapter not created');
});
```

### Creating a Custom Adapter

Implement `IAnalyticsAdapter` and register it with `setAdapter`:

```typescript
import type { IAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';
import type { AnalyticsEvent } from '@equinor/fusion-framework-module-analytics';

class MyRemoteAdapter implements IAnalyticsAdapter {
registerAnalytic(event: AnalyticsEvent): void {
navigator.sendBeacon('/analytics', JSON.stringify(event));
}

[Symbol.dispose](): void {
// cleanup if needed
}
}

builder.setAdapter('remote', async () => new MyRemoteAdapter());
```

## Collectors

Collectors implement `IAnalyticsCollector` (or extend `BaseCollector`) and emit
`AnalyticsEvent` objects that are forwarded to all adapters. All collectors
support async initialisation.

### ContextSelectedCollector

Emits an event when the active Fusion context changes. Includes the new context,
the previous context, and the current app key in attributes.

```typescript
builder.setCollector('context-selected', async (args) => {
const ctx = await args.requireInstance('context');
const app = await args.requireInstance('app');
return new ContextSelectedCollector(ctx, app);
});
```

### AppSelectedCollector

Emits an event when the active application changes. Includes the new and
previous app key metadata.

```typescript
builder.setCollector('app-selected', async (args) => {
const app = await args.requireInstance('app');
return new AppSelectedCollector(app);
});
```

### AppLoadedCollector

Emits an event when an application's modules finish loading. Includes app
manifest metadata and the current context (if available).

```typescript
builder.setCollector('app-loaded', async (args) => {
const event = await args.requireInstance('event');
const app = await args.requireInstance('app');
return new AppLoadedCollector(event, app);
});
```

### Creating a Custom Collector

Extend `BaseCollector` with a Zod schema for validation:

```typescript
import { BaseCollector, createSchema } from '@equinor/fusion-framework-module-analytics/collectors';
import { z } from 'zod';
import { of } from 'rxjs';

const schema = createSchema(z.string(), z.object({ page: z.string() }));

class PageViewCollector extends BaseCollector<string, { page: string }> {
constructor() {
super('page-view', schema);
}

_initialize() {
return of({ value: window.location.pathname, attributes: { page: document.title } });
}
}
```

## Tracking Events Manually

The provider exposes methods for ad-hoc event tracking outside of collectors:

```typescript
// Single event
provider.trackAnalytic({
name: 'button-click',
value: 'save',
attributes: { section: 'toolbar' },
});

// Observable stream
const subscription = provider.trackAnalytic$(myEvent$);
// later: subscription.unsubscribe();
```

#### Configuration

The Context Selected Collector needs the context provider.

##### Example configuration

```typescript
import { enableAnalytics } from '@equinor/fusion-framework-module-analytics';
import { ContextSelectedCollector } from '@equinor/fusion-framework-module-analytics/collectors';

const configure = (configurator: IModulesConfigurator<any, any>) => {
enableAnalytics(configurator, (builder) => {
builder.setCollector('context-selected', async (args) => {
const contextProvider = await args.requireInstance('context');
const appProvider = await args.requireInstance('app');
return new ContextSelectedCollector(contextProvider, appProvider);
});
});
}
```

### Creating Custom Collectors

You can create custom analytics collector by extending the `BaseCollector` class,
or implement the `IAnalyticsCollector` interface and add it in configuration.

#### Example Custom Collector

```typescript
import { type AnalyticsEvent, enableAnalytics } from '@equinor/fusion-framework-module-analytics';

const configure = (configurator: IModulesConfigurator<any, any>) => {
enableAnalytics(configurator, (builder) => {
builder.setCollector('click-test', async () => {
const subject = new Subject<AnalyticsEvent>();
window.addEventListener('click', (e) => {
subject.next({
name: 'window-clicker',
value: 42,
});
});

return {
subscribe: (subscriber) => {
return subject.subscribe(subscriber);
},
};
});
});
}
```
See [Adapters](docs/adapters.md), [Collectors](docs/collectors.md), and
[Tracking Events Manually](docs/tracking-events.md) for the full adapter/collector
reference and how to build your own.
79 changes: 79 additions & 0 deletions packages/modules/analytics/docs/adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Adapters

Adapters implement `IAnalyticsAdapter` and are responsible for processing and
sending analytics data to their destinations. All adapters support async
initialisation and will be initialised automatically when the provider starts.

## ConsoleAnalyticsAdapter

Logs every analytics event to the browser console. Useful for development and
debugging. No configuration required.

```typescript
builder.setAdapter('console', async () => new ConsoleAnalyticsAdapter());
```

## FusionAnalyticsAdapter

Forwards analytics events to an OpenTelemetry-compatible log endpoint via a
bundled `LoggerProvider`.

Configuration options:

| Option | Type | Description |
|---|---|---|
| `portalId` | `string` | Portal identifier included in every log record |
| `logExporter` | `OTLPExporterBase` | OTLP log exporter for transport |

### Using `OTLPLogExporter` (direct HTTP)

```typescript
import { OTLPLogExporter } from '@equinor/fusion-framework-module-analytics/logExporters';
import { FusionAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';

builder.setAdapter('fusion-log', async () => {
const logExporter = new OTLPLogExporter({
url: 'https://example.com/v1/logs',
headers: { 'Content-Type': 'application/json' },
});
return new FusionAnalyticsAdapter({ portalId: 'my-portal', logExporter });
});
```

### Using `FusionOTLPLogExporter` (service discovery HTTP client)

```typescript
import { FusionOTLPLogExporter } from '@equinor/fusion-framework-module-analytics/logExporters';
import { FusionAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';

builder.setAdapter('fusion', async (args) => {
if (args.hasModule('serviceDiscovery')) {
const sd = await args.requireInstance('serviceDiscovery');
const httpClient = await sd.createClient('analytics');
const logExporter = new FusionOTLPLogExporter(httpClient);
return new FusionAnalyticsAdapter({ portalId: 'my-portal', logExporter });
}
console.error('Service discovery unavailable — analytics adapter not created');
});
```

## Creating a Custom Adapter

Implement `IAnalyticsAdapter` and register it with `setAdapter`:

```typescript
import type { IAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';
import type { AnalyticsEvent } from '@equinor/fusion-framework-module-analytics';

class MyRemoteAdapter implements IAnalyticsAdapter {
registerAnalytic(event: AnalyticsEvent): void {
navigator.sendBeacon('/analytics', JSON.stringify(event));
}

[Symbol.dispose](): void {
// cleanup if needed
}
}

builder.setAdapter('remote', async () => new MyRemoteAdapter());
```
Loading
Loading