Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions apps/docs/resources/telemetry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const editor = await Editor.open(file, {
Custom key-value pairs included with every event. Use this to attach your own identifiers (customer ID, environment, etc.).
</ParamField>

<ParamField path="telemetry.licenseKey" type="string" deprecated>
**Deprecated.** Use the root-level `licenseKey` instead. If both are provided, the root-level key takes priority.
</ParamField>

### Disabling telemetry

Set `enabled: false` to turn telemetry off entirely:
Expand All @@ -107,6 +111,28 @@ const superdoc = new SuperDoc({

No network requests will be made. Document metadata (GUID and timestamp) is still generated locally so files export correctly.

### Self-reporting

You can route telemetry through your own server using `telemetry.endpoint`. SuperDoc sends the same JSON payload to whatever URL you provide — inspect it, store it, forward it, or drop it.

```javascript
const superdoc = new SuperDoc({
selector: '#editor',
document: yourFile,
licenseKey: 'your-license-key',
telemetry: {
enabled: true,
endpoint: 'https://your-server.com/superdoc/telemetry',
},
});
```

Your server receives a `POST` with the payload described in [Payload example](#payload-example). From there you can aggregate usage on your side — count document opens per customer, track adoption across teams, or feed the data into your own billing pipeline.

<Note>
A dedicated self-reporting API with dashboards and usage summaries is under development and will be available by Feb 28, 2026. In the meantime, proxying via `telemetry.endpoint` gives you full control over the raw data.
</Note>

## License key

The license key identifies your organization. It's sent as an `X-License-Key` header with every telemetry request.
Expand Down
58 changes: 56 additions & 2 deletions packages/super-editor/src/core/Editor.telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ vi.mock('@superdoc/common', () => ({
// Test the telemetry initialization logic in isolation
// This mirrors the #initTelemetry method in Editor.ts
function initTelemetry(options: {
telemetry?: { enabled: boolean; endpoint?: string; metadata?: Record<string, unknown> } | null;
telemetry?: {
enabled: boolean;
endpoint?: string;
metadata?: Record<string, unknown>;
licenseKey?: string | null;
} | null;
licenseKey?: string;
}): Telemetry | null {
const { telemetry: telemetryConfig, licenseKey } = options;
Expand All @@ -22,11 +27,15 @@ function initTelemetry(options: {
return null;
}

// Root-level licenseKey has a priority; fall back to deprecated telemetry.licenseKey
const resolvedLicenseKey =
licenseKey !== undefined ? licenseKey : (telemetryConfig.licenseKey ?? COMMUNITY_LICENSE_KEY);

try {
return new Telemetry({
enabled: true,
endpoint: telemetryConfig.endpoint,
licenseKey: licenseKey === undefined ? COMMUNITY_LICENSE_KEY : licenseKey,
licenseKey: resolvedLicenseKey,
metadata: telemetryConfig.metadata,
});
} catch {
Expand Down Expand Up @@ -122,6 +131,51 @@ describe('Editor Telemetry Integration', () => {
});
});

describe('deprecated telemetry.licenseKey', () => {
it('uses telemetry.licenseKey when root licenseKey is not provided', () => {
const result = initTelemetry({
telemetry: { enabled: true, licenseKey: 'deprecated-key' },
});

expect(result).not.toBeNull();
expect(Telemetry).toHaveBeenCalledWith({
enabled: true,
endpoint: undefined,
licenseKey: 'deprecated-key',
metadata: undefined,
});
});

it('root licenseKey wins over telemetry.licenseKey', () => {
const result = initTelemetry({
telemetry: { enabled: true, licenseKey: 'deprecated-key' },
licenseKey: 'root-key',
});

expect(result).not.toBeNull();
expect(Telemetry).toHaveBeenCalledWith({
enabled: true,
endpoint: undefined,
licenseKey: 'root-key',
metadata: undefined,
});
});

it('falls back to COMMUNITY_LICENSE_KEY when both are absent', () => {
const result = initTelemetry({
telemetry: { enabled: true },
});

expect(result).not.toBeNull();
expect(Telemetry).toHaveBeenCalledWith({
enabled: true,
endpoint: undefined,
licenseKey: 'community-and-eval-agplv3',
metadata: undefined,
});
});
});

describe('telemetry with custom endpoint', () => {
it('passes custom endpoint to Telemetry', () => {
const customEndpoint = 'https://custom.telemetry.com/v1/events';
Expand Down
10 changes: 7 additions & 3 deletions packages/super-editor/src/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ export class Editor extends EventEmitter<EditorEventMap> {
// header/footer editors may have parent(main) editor set
parentEditor: null,

// License key (defaults to community license)
licenseKey: COMMUNITY_LICENSE_KEY,
// License key (resolved in #initTelemetry; undefined means "not explicitly set")
licenseKey: undefined,

// Telemetry configuration
telemetry: { enabled: true },
Expand Down Expand Up @@ -493,11 +493,15 @@ export class Editor extends EventEmitter<EditorEventMap> {
return;
}

// Root-level licenseKey has a priority; fall back to deprecated telemetry.licenseKey
const resolvedLicenseKey =
licenseKey !== undefined ? licenseKey : (telemetryConfig.licenseKey ?? COMMUNITY_LICENSE_KEY);

try {
this.#telemetry = new Telemetry({
enabled: true,
endpoint: telemetryConfig.endpoint,
licenseKey: licenseKey === undefined ? COMMUNITY_LICENSE_KEY : licenseKey,
licenseKey: resolvedLicenseKey,
metadata: telemetryConfig.metadata,
});
console.debug('[super-editor] Telemetry: enabled');
Expand Down
4 changes: 4 additions & 0 deletions packages/super-editor/src/core/types/EditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,5 +432,9 @@ export interface EditorOptions {
endpoint?: string;
/** Custom metadata to include with telemetry events (optional) */
metadata?: Record<string, unknown>;
/**
* @deprecated Use root-level `licenseKey` instead. If both are provided, root-level has priority.
*/
licenseKey?: string | null;
} | null;
}
1 change: 1 addition & 0 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ const editorOptions = (doc) => {
enabled: true,
endpoint: proxy.$superdoc.config.telemetry?.endpoint,
metadata: proxy.$superdoc.config.telemetry?.metadata,
licenseKey: proxy.$superdoc.config.telemetry?.licenseKey,
}
: null,
};
Expand Down
6 changes: 3 additions & 3 deletions packages/superdoc/src/core/SuperDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EventEmitter } from 'eventemitter3';
import { v4 as uuidv4 } from 'uuid';
import { HocuspocusProviderWebsocket } from '@hocuspocus/provider';

import { DOCX, PDF, HTML, COMMUNITY_LICENSE_KEY } from '@superdoc/common';
import { DOCX, PDF, HTML } from '@superdoc/common';
import { SuperToolbar, createZip } from '@superdoc/super-editor';
import { SuperComments } from '../components/CommentsLayer/commentsList/super-comments-list.js';
import { createSuperdocVueApp } from './create-app.js';
Expand Down Expand Up @@ -79,8 +79,8 @@ export class SuperDoc extends EventEmitter {
modules: {}, // Optional: Modules to load. Use modules.ai.{your_key} to pass in your key
permissionResolver: null, // Optional: Override for permission checks

// License key for organization identification (defaults to community key)
licenseKey: COMMUNITY_LICENSE_KEY,
// License key (resolved downstream; undefined means "not explicitly set")
licenseKey: undefined,

// Telemetry settings
telemetry: { enabled: true },
Expand Down
6 changes: 6 additions & 0 deletions packages/superdoc/src/core/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@
* @property {boolean} [isDebug=false] Whether to enable debug mode
* @property {ViewOptions} [viewOptions] Document view options (OOXML ST_View compatible)
* @property {string} [cspNonce] Content Security Policy nonce for dynamically injected styles
* @property {string} [licenseKey] License key for organization identification
* @property {{ enabled: boolean, endpoint?: string, metadata?: Record<string, unknown>, licenseKey?: string }} [telemetry] Telemetry configuration
* @property {boolean} telemetry.enabled Whether telemetry is enabled
* @property {string} [telemetry.endpoint] Custom telemetry endpoint
* @property {Record<string, unknown>} [telemetry.metadata] Custom metadata to include with telemetry events
* @property {string} [telemetry.licenseKey] @deprecated Use root-level `licenseKey` instead
*/

export {};
34 changes: 33 additions & 1 deletion packages/superdoc/src/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ function transformTelemetryConfig(superdocConfig: {
enabled?: boolean;
endpoint?: string;
metadata?: Record<string, unknown>;
licenseKey?: string | null;
} | null;
licenseKey?: string | null;
}): {
telemetry: { enabled: boolean; endpoint?: string; metadata?: Record<string, unknown> } | null;
telemetry: {
enabled: boolean;
endpoint?: string;
metadata?: Record<string, unknown>;
licenseKey?: string | null;
} | null;
licenseKey?: string | null;
} {
return {
Expand All @@ -20,6 +26,7 @@ function transformTelemetryConfig(superdocConfig: {
enabled: true,
endpoint: superdocConfig.telemetry?.endpoint,
metadata: superdocConfig.telemetry?.metadata,
licenseKey: superdocConfig.telemetry?.licenseKey,
}
: null,
};
Expand Down Expand Up @@ -144,6 +151,31 @@ describe('SuperDoc Telemetry Configuration', () => {
});
});

describe('deprecated telemetry.licenseKey passthrough', () => {
it('passes deprecated telemetry.licenseKey to editor config', () => {
const result = transformTelemetryConfig({
telemetry: { enabled: true, licenseKey: 'deprecated-key' },
});

expect(result.telemetry).toEqual({
enabled: true,
endpoint: undefined,
metadata: undefined,
licenseKey: 'deprecated-key',
});
});

it('passes both root licenseKey and deprecated telemetry.licenseKey', () => {
const result = transformTelemetryConfig({
telemetry: { enabled: true, licenseKey: 'deprecated-key' },
licenseKey: 'root-key',
});

expect(result.licenseKey).toBe('root-key');
expect(result.telemetry?.licenseKey).toBe('deprecated-key');
});
});

describe('full configuration flow', () => {
it('transforms complete SuperDoc config to Editor config', () => {
const superdocConfig = {
Expand Down
Loading