Files
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

322 lines
10 KiB
TypeScript

import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Response } from 'node-fetch';
import { TelemetryConfig, TelemetryService } from '../../src/services/telemetry/telemetry.service';
import logger from '../../src/utils/logger';
type FetchFunction = ConstructorParameters<typeof TelemetryService>[1];
const tempRoots: string[] = [];
const ciEnvKeys = ['CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'BUILDKITE', 'CIRCLECI', 'JENKINS_URL'];
let savedEnv: NodeJS.ProcessEnv;
beforeEach(() => {
savedEnv = { ...process.env };
});
afterEach(() => {
process.env = savedEnv;
vi.restoreAllMocks();
for (const tempRoot of tempRoots.splice(0)) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
function makeConfig(overrides: Partial<TelemetryConfig> = {}): TelemetryConfig {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'insforge-telemetry-'));
tempRoots.push(tempRoot);
return {
disabled: false,
endpoint: 'https://telemetry.test/v1/events',
posthogApiKey: 'phc_test',
installationIdPath: path.join(tempRoot, '.insforge-installation-id'),
heartbeatIntervalMs: 60_000,
requestTimeoutMs: 500,
...overrides,
};
}
function makeFetchMock(status = 204): FetchFunction {
return vi.fn(async () => new Response(null, { status })) as FetchFunction;
}
function clearRuntimeEnvironment(): void {
for (const key of ciEnvKeys) {
delete process.env[key];
}
delete process.env.NODE_ENV;
delete process.env.npm_lifecycle_event;
}
function getPostedBody(fetchMock: FetchFunction, callIndex = 0): Record<string, unknown> {
const call = vi.mocked(fetchMock).mock.calls[callIndex];
expect(call).toBeDefined();
const init = call?.[1];
expect(init).toBeDefined();
expect(typeof init).toBe('object');
const body = (init as { body?: unknown }).body;
expect(typeof body).toBe('string');
return JSON.parse(body as string) as Record<string, unknown>;
}
describe('TelemetryService', () => {
it('does not create an installation id or send events when disabled', async () => {
const config = makeConfig({ disabled: true });
const fetchMock = makeFetchMock();
await new TelemetryService(config, fetchMock).sendEvent('heartbeat');
expect(fetchMock).not.toHaveBeenCalled();
expect(fs.existsSync(config.installationIdPath)).toBe(false);
});
it('persists one anonymous installation id and reuses it across events', async () => {
const config = makeConfig();
const fetchMock = makeFetchMock();
const service = new TelemetryService(config, fetchMock);
await service.sendEvent('instance_started');
const installationId = fs.readFileSync(config.installationIdPath, 'utf8').trim();
const firstBody = getPostedBody(fetchMock);
await service.sendEvent('heartbeat');
const secondBody = getPostedBody(fetchMock, 1);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(firstBody.event).toBe('oss_instance_started');
expect(secondBody.event).toBe('oss_heartbeat');
expect(secondBody.distinct_id).toBe(installationId);
expect(installationId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
);
});
it('reuses an installation id published by another process during the atomic create', async () => {
const config = makeConfig();
const fetchMock = makeFetchMock();
const racedInstallationId = '11111111-1111-4111-8111-111111111111';
const linkSync = fs.linkSync.bind(fs);
const linkSpy = vi.spyOn(fs, 'linkSync').mockImplementation((existingPath, newPath) => {
if (newPath === config.installationIdPath) {
fs.writeFileSync(config.installationIdPath, racedInstallationId, { mode: 0o600 });
throw Object.assign(new Error('file exists'), { code: 'EEXIST' });
}
return linkSync(existingPath, newPath);
});
await new TelemetryService(config, fetchMock).sendEvent('instance_started');
const body = getPostedBody(fetchMock);
expect(linkSpy).toHaveBeenCalled();
expect(body.distinct_id).toBe(racedInstallationId);
expect(fs.readFileSync(config.installationIdPath, 'utf8')).toBe(racedInstallationId);
expect(fs.readdirSync(path.dirname(config.installationIdPath))).toEqual([
path.basename(config.installationIdPath),
]);
});
it('marks CI telemetry with the CI runtime environment', async () => {
process.env.CI = 'true';
const config = makeConfig();
const fetchMock = makeFetchMock();
await new TelemetryService(config, fetchMock).sendEvent('instance_started');
const body = getPostedBody(fetchMock);
expect(body.properties).toEqual(
expect.objectContaining({
runtime_environment: 'ci',
is_ci: true,
})
);
});
it('marks local development telemetry with the development runtime environment', async () => {
clearRuntimeEnvironment();
process.env.npm_lifecycle_event = 'dev';
const config = makeConfig();
const fetchMock = makeFetchMock();
await new TelemetryService(config, fetchMock).sendEvent('instance_started');
const body = getPostedBody(fetchMock);
expect(body.properties).toEqual(
expect.objectContaining({
runtime_environment: 'development',
is_ci: false,
})
);
});
it.each([
{
name: 'CI takes precedence over NODE_ENV',
setup: () => {
clearRuntimeEnvironment();
process.env.CI = 'true';
process.env.NODE_ENV = 'test';
},
expectedRuntimeEnvironment: 'ci',
expectedIsCi: true,
},
{
name: 'production NODE_ENV',
setup: () => {
clearRuntimeEnvironment();
process.env.NODE_ENV = 'production';
},
expectedRuntimeEnvironment: 'production',
expectedIsCi: false,
},
{
name: 'development NODE_ENV',
setup: () => {
clearRuntimeEnvironment();
process.env.NODE_ENV = 'development';
},
expectedRuntimeEnvironment: 'development',
expectedIsCi: false,
},
{
name: 'test NODE_ENV',
setup: () => {
clearRuntimeEnvironment();
process.env.NODE_ENV = 'test';
},
expectedRuntimeEnvironment: 'test',
expectedIsCi: false,
},
{
name: 'unknown runtime',
setup: clearRuntimeEnvironment,
expectedRuntimeEnvironment: 'unknown',
expectedIsCi: false,
},
])(
'marks telemetry with the $name runtime environment',
async ({ setup, expectedRuntimeEnvironment, expectedIsCi }) => {
setup();
const config = makeConfig();
const fetchMock = makeFetchMock();
await new TelemetryService(config, fetchMock).sendEvent('instance_started');
const body = getPostedBody(fetchMock);
expect(body.properties).toEqual(
expect.objectContaining({
runtime_environment: expectedRuntimeEnvironment,
is_ci: expectedIsCi,
})
);
}
);
it('starts once, schedules heartbeats, and stops the heartbeat timer', async () => {
vi.useFakeTimers();
try {
const config = makeConfig({ heartbeatIntervalMs: 1_000 });
const fetchMock = makeFetchMock();
const service = new TelemetryService(config, fetchMock);
service.start();
await Promise.resolve();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(getPostedBody(fetchMock).event).toBe('oss_instance_started');
service.start();
await Promise.resolve();
await Promise.resolve();
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(config.heartbeatIntervalMs);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(getPostedBody(fetchMock, 1).event).toBe('oss_heartbeat');
service.stop();
await vi.advanceTimersByTimeAsync(config.heartbeatIntervalMs);
expect(fetchMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it('sends only coarse, non-sensitive runtime fields', async () => {
const config = makeConfig();
const fetchMock = makeFetchMock();
await new TelemetryService(config, fetchMock).sendEvent('heartbeat');
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock).toHaveBeenCalledWith(
'https://telemetry.test/v1/events',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'content-type': 'application/json',
}),
})
);
const body = getPostedBody(fetchMock);
expect(body).toEqual({
api_key: 'phc_test',
event: 'oss_heartbeat',
distinct_id: expect.any(String),
timestamp: expect.any(String),
properties: {
$process_person_profile: false,
installation_id: expect.any(String),
telemetry_source: 'insforge_oss',
telemetry_event_name: 'heartbeat',
version: expect.any(String),
hosting_mode: expect.stringMatching(/^(cloud|self-hosted)$/),
deployment_method: expect.any(String),
platform: process.platform,
arch: process.arch,
node_version: process.version,
runtime_environment: expect.stringMatching(/^(production|development|test|ci|unknown)$/),
is_ci: expect.any(Boolean),
storage_backend: expect.stringMatching(/^(local|s3|s3-compatible)$/),
features: {
site_deployments_configured: expect.any(Boolean),
functions_configured: expect.any(Boolean),
compute_configured: expect.any(Boolean),
openrouter_configured: expect.any(Boolean),
},
},
});
expect(JSON.stringify(body)).not.toContain('JWT_SECRET');
expect(JSON.stringify(body)).not.toContain('ACCESS_API_KEY');
expect(JSON.stringify(body)).not.toContain('POSTGRES_PASSWORD');
});
it('logs and suppresses network errors', async () => {
const config = makeConfig();
const fetchMock = vi.fn(async () => {
throw new Error('network down');
}) as FetchFunction;
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => logger);
await expect(new TelemetryService(config, fetchMock).sendEvent('heartbeat')).resolves.toBe(
undefined
);
expect(warnSpy).toHaveBeenCalledWith(
'InsForge telemetry skipped',
expect.objectContaining({ error: 'network down' })
);
});
});