feat(mosaic): mosaic telemetry command (M6 CU-06-01..05) (#417)
This commit was merged in pull request #417.
This commit is contained in:
112
packages/mosaic/src/telemetry/consent-store.ts
Normal file
112
packages/mosaic/src/telemetry/consent-store.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Persistent consent store for remote telemetry upload.
|
||||
*
|
||||
* State is stored in $MOSAIC_HOME/telemetry.json (not inside the markdown
|
||||
* config files — those are template-rendered and would lose structured data).
|
||||
*
|
||||
* Schema:
|
||||
* {
|
||||
* remoteEnabled: boolean,
|
||||
* optedInAt: string | null, // ISO timestamp
|
||||
* optedOutAt: string | null, // ISO timestamp
|
||||
* lastUploadAt: string | null // ISO timestamp
|
||||
* }
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { atomicWrite } from '../platform/file-ops.js';
|
||||
import { DEFAULT_MOSAIC_HOME } from '../constants.js';
|
||||
|
||||
export interface TelemetryConsent {
|
||||
remoteEnabled: boolean;
|
||||
optedInAt: string | null;
|
||||
optedOutAt: string | null;
|
||||
lastUploadAt: string | null;
|
||||
}
|
||||
|
||||
const TELEMETRY_FILE = 'telemetry.json';
|
||||
|
||||
const DEFAULT_CONSENT: TelemetryConsent = {
|
||||
remoteEnabled: false,
|
||||
optedInAt: null,
|
||||
optedOutAt: null,
|
||||
lastUploadAt: null,
|
||||
};
|
||||
|
||||
function consentFilePath(mosaicHome?: string): string {
|
||||
return join(mosaicHome ?? getMosaicHome(), TELEMETRY_FILE);
|
||||
}
|
||||
|
||||
function getMosaicHome(): string {
|
||||
return process.env['MOSAIC_HOME'] ?? DEFAULT_MOSAIC_HOME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current consent state. Returns defaults if file doesn't exist.
|
||||
*/
|
||||
export function readConsent(mosaicHome?: string): TelemetryConsent {
|
||||
const filePath = consentFilePath(mosaicHome);
|
||||
if (!existsSync(filePath)) {
|
||||
return { ...DEFAULT_CONSENT };
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as Partial<TelemetryConsent>;
|
||||
return {
|
||||
remoteEnabled: parsed.remoteEnabled ?? false,
|
||||
optedInAt: parsed.optedInAt ?? null,
|
||||
optedOutAt: parsed.optedOutAt ?? null,
|
||||
lastUploadAt: parsed.lastUploadAt ?? null,
|
||||
};
|
||||
} catch {
|
||||
return { ...DEFAULT_CONSENT };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a full or partial consent update.
|
||||
*/
|
||||
export function writeConsent(update: Partial<TelemetryConsent>, mosaicHome?: string): void {
|
||||
const current = readConsent(mosaicHome);
|
||||
const next: TelemetryConsent = { ...current, ...update };
|
||||
atomicWrite(consentFilePath(mosaicHome), JSON.stringify(next, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark opt-in: enable remote upload and record timestamp.
|
||||
*/
|
||||
export function optIn(mosaicHome?: string): TelemetryConsent {
|
||||
const now = new Date().toISOString();
|
||||
const next: TelemetryConsent = {
|
||||
remoteEnabled: true,
|
||||
optedInAt: now,
|
||||
optedOutAt: null,
|
||||
lastUploadAt: readConsent(mosaicHome).lastUploadAt,
|
||||
};
|
||||
writeConsent(next, mosaicHome);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark opt-out: disable remote upload and record timestamp.
|
||||
*/
|
||||
export function optOut(mosaicHome?: string): TelemetryConsent {
|
||||
const now = new Date().toISOString();
|
||||
const current = readConsent(mosaicHome);
|
||||
const next: TelemetryConsent = {
|
||||
remoteEnabled: false,
|
||||
optedInAt: current.optedInAt,
|
||||
optedOutAt: now,
|
||||
lastUploadAt: current.lastUploadAt,
|
||||
};
|
||||
writeConsent(next, mosaicHome);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a successful upload timestamp.
|
||||
*/
|
||||
export function recordUpload(mosaicHome?: string): void {
|
||||
writeConsent({ lastUploadAt: new Date().toISOString() }, mosaicHome);
|
||||
}
|
||||
Reference in New Issue
Block a user