mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
Emails, the signing page's title, the sealed PDF's reason, the sign-in refusal and the dialogs still said Hanzo Sign. The catalogs change with the strings, message for message, so every translation keeps its id.
191 lines
5.3 KiB
TypeScript
191 lines
5.3 KiB
TypeScript
/* eslint-disable require-atomic-updates */
|
|
import { Insights as InsightsClient } from '@hanzo/insights-node';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { version } from '../../../../package.json';
|
|
import { prefixedId } from '../../universal/id';
|
|
import { getSiteSetting } from '../site-settings/get-site-setting';
|
|
import { SITE_SETTINGS_TELEMETRY_ID } from '../site-settings/schemas/telemetry';
|
|
import { upsertSiteSetting } from '../site-settings/upsert-site-setting';
|
|
|
|
const TELEMETRY_KEY = process.env.NEXT_PRIVATE_TELEMETRY_KEY;
|
|
const TELEMETRY_HOST = process.env.NEXT_PRIVATE_TELEMETRY_HOST;
|
|
const TELEMETRY_DISABLED = !!process.env.SIGN_DISABLE_TELEMETRY;
|
|
|
|
const NODE_ID_FILENAME = '.sign-node-id';
|
|
const HEARTBEAT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
|
|
// Version is hardcoded to avoid rollup JSON import issues
|
|
const APP_VERSION = version;
|
|
|
|
export class TelemetryClient {
|
|
private static instance: TelemetryClient | null = null;
|
|
|
|
private client: InsightsClient | null = null;
|
|
|
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
|
|
private installationId: string | null = null;
|
|
private nodeId: string | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
/**
|
|
* Start the telemetry client.
|
|
*
|
|
* This will initialize the Insights client, load or create the installation ID and node ID,
|
|
* capture a startup event, and start a heartbeat interval.
|
|
*
|
|
* If telemetry is disabled via `SIGN_DISABLE_TELEMETRY=true` or credentials are not
|
|
* provided, this will be a no-op.
|
|
*/
|
|
public static async start(): Promise<void> {
|
|
if (TELEMETRY_DISABLED) {
|
|
console.log(
|
|
'[Telemetry] Telemetry is disabled. To enable, remove the SIGN_DISABLE_TELEMETRY environment variable.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!TELEMETRY_KEY || !TELEMETRY_HOST) {
|
|
console.log('[Telemetry] Telemetry credentials not configured. Telemetry will not be sent.');
|
|
return;
|
|
}
|
|
|
|
if (TelemetryClient.instance) {
|
|
return;
|
|
}
|
|
|
|
const instance = new TelemetryClient();
|
|
|
|
TelemetryClient.instance = instance;
|
|
|
|
await instance.initialize();
|
|
}
|
|
|
|
/**
|
|
* Stop the telemetry client.
|
|
*
|
|
* This will clear the heartbeat interval and shutdown the Insights client.
|
|
*/
|
|
public static async stop(): Promise<void> {
|
|
const instance = TelemetryClient.instance;
|
|
|
|
if (!instance) {
|
|
return;
|
|
}
|
|
|
|
if (instance.heartbeatInterval) {
|
|
clearInterval(instance.heartbeatInterval);
|
|
}
|
|
|
|
if (instance.client) {
|
|
await instance.client.shutdown();
|
|
}
|
|
|
|
TelemetryClient.instance = null;
|
|
}
|
|
|
|
private async initialize(): Promise<void> {
|
|
this.client = new InsightsClient(TELEMETRY_KEY!, {
|
|
host: TELEMETRY_HOST,
|
|
disableGeoip: false,
|
|
});
|
|
|
|
// Load or create IDs
|
|
this.installationId = await this.getOrCreateInstallationId();
|
|
this.nodeId = await this.getOrCreateNodeId();
|
|
|
|
console.log(
|
|
'[Telemetry] Telemetry is enabled. Hanzo eSign collects anonymous usage data to help improve the product.',
|
|
);
|
|
console.log(
|
|
'[Telemetry] We collect: app version, installation ID, and node ID. No personal data, document contents, or user information is collected.',
|
|
);
|
|
console.log(
|
|
'[Telemetry] To disable telemetry, set SIGN_DISABLE_TELEMETRY=true in your environment variables.',
|
|
);
|
|
console.log(
|
|
'[Telemetry] Learn more: https://esign.hanzo.ai/docs/developers/self-hosting/telemetry',
|
|
);
|
|
|
|
// Capture startup event
|
|
this.captureEvent('telemetry_selfhoster_startup');
|
|
|
|
// Start heartbeat
|
|
this.heartbeatInterval = setInterval(() => {
|
|
this.captureEvent('telemetry_selfhoster_heartbeat');
|
|
}, HEARTBEAT_INTERVAL_MS);
|
|
}
|
|
|
|
private captureEvent(event: string): void {
|
|
if (!this.client || !this.installationId) {
|
|
return;
|
|
}
|
|
|
|
this.client.capture({
|
|
distinctId: this.installationId,
|
|
event,
|
|
properties: {
|
|
appVersion: APP_VERSION,
|
|
installationId: this.installationId,
|
|
nodeId: this.nodeId,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async getOrCreateInstallationId(): Promise<string> {
|
|
try {
|
|
// Try to get from site settings
|
|
const existing = await getSiteSetting({ id: SITE_SETTINGS_TELEMETRY_ID }).catch(() => null);
|
|
|
|
if (existing) {
|
|
if (existing.data.installationId) {
|
|
return existing.data.installationId;
|
|
}
|
|
}
|
|
|
|
// Create new installation ID
|
|
const installationId = prefixedId('installation');
|
|
|
|
await upsertSiteSetting({
|
|
id: SITE_SETTINGS_TELEMETRY_ID,
|
|
data: { installationId },
|
|
enabled: true,
|
|
});
|
|
|
|
return installationId;
|
|
} catch {
|
|
// If database is not available, generate a temporary ID
|
|
return prefixedId('installation');
|
|
}
|
|
}
|
|
|
|
private async getOrCreateNodeId(): Promise<string | null> {
|
|
const nodeIdPath = path.join(os.tmpdir(), NODE_ID_FILENAME);
|
|
|
|
try {
|
|
const existingId = await fs.readFile(nodeIdPath, 'utf-8');
|
|
|
|
if (existingId.trim()) {
|
|
return existingId.trim();
|
|
}
|
|
} catch {
|
|
// File doesn't exist or can't be read, continue to create
|
|
}
|
|
|
|
// Generate new node ID
|
|
const nodeId = prefixedId('node');
|
|
|
|
try {
|
|
await fs.writeFile(nodeIdPath, nodeId, 'utf-8');
|
|
} catch {
|
|
// Read-only filesystem, use memory for nodeId
|
|
}
|
|
|
|
return nodeId;
|
|
}
|
|
}
|