import { Injectable } from '@nestjs/common';
import { AuditService } from '../../audit/audit.service';
import { DatabaseService } from '../../../infrastructure/database/database.service';
import {
assertTelemetryOrganizationId,
assertTelemetrySettingsUpdateInput,
normalizeTelemetryAuditLimit,
} from './telemetry-input-validation';
export interface TelemetrySettings {
automaticTechnicalDiagnostics: boolean;
crashReporting: boolean;
performanceTelemetry: boolean;
productWorkflowAnalytics: boolean;
extendedPilotDiagnostics: boolean;
manualSupportBundleUploads: boolean;
pilotMode: boolean;
}
export const TELEMETRY_REMOTE_CATEGORY_KEYS = [
'automaticTechnicalDiagnostics',
'crashReporting',
'performanceTelemetry',
'productWorkflowAnalytics',
'extendedPilotDiagnostics',
'manualSupportBundleUploads',
] as const;
export type TelemetryRemoteCategory = (typeof TELEMETRY_REMOTE_CATEGORY_KEYS)[number];
export interface TelemetryCategoryMetadata {
key: TelemetryRemoteCategory;
label: string;
dataCollected: string;
purpose: string;
storage: 'local_only' | 'remote_when_enabled' | 'manual_remote_only';
retention: string;
roles: string[];
}
export interface TelemetryMetadata {
categories: TelemetryCategoryMetadata[];
pilotMode: {label: string; activation: string; retention: string};
localOperationalLogging: {separateFromRemoteTelemetry: true; purpose: string; access: string};
}
export interface TelemetrySettingsAuditEntry {
timestamp: string;
changedByRole: string;
previousPolicy: TelemetrySettings;
newPolicy: TelemetrySettings;
}
const defaults: TelemetrySettings = {
automaticTechnicalDiagnostics: false,
crashReporting: false,
performanceTelemetry: false,
productWorkflowAnalytics: false,
extendedPilotDiagnostics: false,
manualSupportBundleUploads: false,
pilotMode: false,
};
@Injectable()
export class TelemetrySettingsService {
constructor(private readonly database: DatabaseService, private readonly audit: AuditService) {}
async get(organizationId: string): Promise<TelemetrySettings> {
assertTelemetryOrganizationId(organizationId);
const rows = await this.database.query<TelemetrySettings>(
`select automatic_technical_diagnostics as "automaticTechnicalDiagnostics", crash_reporting as "crashReporting", performance_telemetry as "performanceTelemetry", product_workflow_analytics as "productWorkflowAnalytics", extended_pilot_diagnostics as "extendedPilotDiagnostics", manual_support_bundle_uploads as "manualSupportBundleUploads", pilot_mode as "pilotMode" from telemetry_settings where organization_id=$1 limit 1`,
[organizationId],
);
return rows[0] ? {...defaults, ...rows[0]} : {...defaults};
}
async listAudit(organizationId: string, requestedLimit = 25): Promise<TelemetrySettingsAuditEntry[]> {
assertTelemetryOrganizationId(organizationId);
const limit = normalizeTelemetryAuditLimit(requestedLimit);
const rows = await this.database.query<{
timestamp: string | Date;
changedByRole: unknown;
previousPolicy: unknown;
newPolicy: unknown;
}>(
`select occurred_at as "timestamp",
metadata->>'changedByRole' as "changedByRole",
metadata->'previousPolicy' as "previousPolicy",
metadata->'newPolicy' as "newPolicy"
from audit_events
where organization_id=$1
and event_type='telemetry.settings.updated'
and entity_type='TelemetrySettings'
order by occurred_at desc
limit $2`,
[organizationId, limit],
);
return rows.map((row) => ({
timestamp: row.timestamp instanceof Date
? row.timestamp.toISOString()
: String(row.timestamp),
changedByRole: this.safeAuditRole(row.changedByRole),
previousPolicy: this.normalizePolicy(row.previousPolicy),
newPolicy: this.normalizePolicy(row.newPolicy),
}));
}
describe(): TelemetryMetadata {
return {
categories: [
{key: 'automaticTechnicalDiagnostics', label: 'Automatic Technical Diagnostics', dataCollected: 'Technische Fehler- und Zustandsdaten', purpose: 'Stabilität und Fehlerbehebung', storage: 'remote_when_enabled', retention: 'maximal 30 Tage', roles: ['berechtigte Betreiber', 'GastravaOS-Support']},
{key: 'crashReporting', label: 'Crash Reporting', dataCollected: 'Absturzsignaturen ohne fachliche Inhalte', purpose: 'Fehlerbehebung', storage: 'remote_when_enabled', retention: 'maximal 30 Tage', roles: ['GastravaOS-Support']},
{key: 'performanceTelemetry', label: 'Performance Telemetry', dataCollected: 'Antwortzeiten und technische Metriken', purpose: 'Latenz- und Zuverlässigkeitsverbesserung', storage: 'remote_when_enabled', retention: 'maximal 30 Tage', roles: ['GastravaOS-Engineering']},
{key: 'productWorkflowAnalytics', label: 'Product / Workflow Analytics', dataCollected: 'Aggregierte Workflow-Nutzung', purpose: 'Produktverbesserung', storage: 'remote_when_enabled', retention: 'maximal 90 Tage', roles: ['Product Intelligence']},
{key: 'extendedPilotDiagnostics', label: 'Extended Pilot Diagnostics', dataCollected: 'Erweiterte allowlist-basierte Diagnostik', purpose: 'Ausdrückliche Pilot-Auswertung', storage: 'remote_when_enabled', retention: 'maximal 14 Tage', roles: ['benanntes Pilot-/Support-Team']},
{key: 'manualSupportBundleUploads', label: 'Manual Support Bundle Uploads', dataCollected: 'Manuell ausgewähltes Support-Paket', purpose: 'Gezielte Support-Anfrage', storage: 'manual_remote_only', retention: 'gemäß Support-Fall', roles: ['Betreiber', 'GastravaOS-Support']},
],
pilotMode: {label: 'Pilot Mode', activation: 'bewusste Aktivierung; aktiviert keine Kategorie automatisch', retention: 'erweiterte Diagnostik maximal 14 Tage'},
localOperationalLogging: {separateFromRemoteTelemetry: true, purpose: 'Stabilität, Fehlerbehandlung und Audit', access: 'lokale berechtigte Betriebs- und Supportrollen'},
};
}
async update(input: {organizationId: string; actorUserId: string; settings: TelemetrySettings}): Promise<TelemetrySettings> {
assertTelemetrySettingsUpdateInput(input);
const previous = await this.get(input.organizationId);
const next = {...input.settings};
const rows = await this.database.query<TelemetrySettings>(
`insert into telemetry_settings(organization_id,automatic_technical_diagnostics,crash_reporting,performance_telemetry,product_workflow_analytics,extended_pilot_diagnostics,manual_support_bundle_uploads,pilot_mode,updated_by_user_id)
values($1,$2,$3,$4,$5,$6,$7,$8,$9)
on conflict(organization_id) do update set automatic_technical_diagnostics=excluded.automatic_technical_diagnostics,crash_reporting=excluded.crash_reporting,performance_telemetry=excluded.performance_telemetry,product_workflow_analytics=excluded.product_workflow_analytics,extended_pilot_diagnostics=excluded.extended_pilot_diagnostics,manual_support_bundle_uploads=excluded.manual_support_bundle_uploads,pilot_mode=excluded.pilot_mode,updated_by_user_id=excluded.updated_by_user_id,updated_at=now()
returning automatic_technical_diagnostics as "automaticTechnicalDiagnostics", crash_reporting as "crashReporting", performance_telemetry as "performanceTelemetry", product_workflow_analytics as "productWorkflowAnalytics", extended_pilot_diagnostics as "extendedPilotDiagnostics", manual_support_bundle_uploads as "manualSupportBundleUploads", pilot_mode as "pilotMode"`,
[input.organizationId,next.automaticTechnicalDiagnostics,next.crashReporting,next.performanceTelemetry,next.productWorkflowAnalytics,next.extendedPilotDiagnostics,next.manualSupportBundleUploads,next.pilotMode,input.actorUserId],
);
const result = rows[0] ? {...defaults, ...rows[0]} : next;
await this.audit.record({organizationId: input.organizationId, actorUserId: input.actorUserId, eventType: 'telemetry.settings.updated', entityType: 'TelemetrySettings', metadata: {changedByRole: 'authorized_operator', previousPolicy: previous, newPolicy: result}});
return result;
}
allowsRemote(category: keyof TelemetrySettings, settings: TelemetrySettings): boolean {
return this.isRemoteCategory(category) && Boolean(settings) && settings[category] === true;
}
private isRemoteCategory(category: keyof TelemetrySettings): category is TelemetryRemoteCategory {
return (TELEMETRY_REMOTE_CATEGORY_KEYS as readonly string[]).includes(category);
}
private normalizePolicy(value: unknown): TelemetrySettings {
const policy = value && typeof value === 'object'
? value as Record<string, unknown>
: {};
return {
automaticTechnicalDiagnostics: policy.automaticTechnicalDiagnostics === true,
crashReporting: policy.crashReporting === true,
performanceTelemetry: policy.performanceTelemetry === true,
productWorkflowAnalytics: policy.productWorkflowAnalytics === true,
extendedPilotDiagnostics: policy.extendedPilotDiagnostics === true,
manualSupportBundleUploads: policy.manualSupportBundleUploads === true,
pilotMode: policy.pilotMode === true,
};
}
private safeAuditRole(value: unknown): string {
return typeof value === 'string' && /^[a-z_]{1,40}$/.test(value)
? value
: 'authorized_operator';
}
}
