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
5 changes: 5 additions & 0 deletions .changeset/quiet-actions-handle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evlog/telemetry": patch
---

Fix `createGitHubActionsTelemetry()` to expose `set()`, `flush()`, and a live `enabled` getter alongside `run()`.
19 changes: 18 additions & 1 deletion apps/telemetry/server/utils/allowed-tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Tool names accepted by `/api/telemetry/ingest` when no override is configured. */
export const DEFAULT_ALLOWED_TOOLS = ['evlog-cli']
export const DEFAULT_ALLOWED_TOOLS = ['evlog-cli', 'evlog-action']

/**
* Custom field keys accepted per tool. Mirrors `telemetry.set()` calls in each
Expand All @@ -17,6 +17,23 @@ export const DEFAULT_ALLOWED_TOOLS = ['evlog-cli']
* enrichers or map rules change.
*/
export const DEFAULT_ALLOWED_CUSTOM_KEYS: Record<string, string[]> = {
'evlog-action': [
'ghaAction',
'ghaEvent',
'baselineMode',
'checkOutcome',
'commentOutcome',
'packages',
'entryPoints',
'score',
'instrumented',
'partial',
'dark',
'regressions',
'fixed',
'gatePassed',
'baselineDelta',
],
'evlog-cli': [
// evlog doctor — which part of a setup people get stuck on
'checksFailed',
Expand Down
58 changes: 58 additions & 0 deletions apps/telemetry/test/action-ingest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { parseIngestBody } from '@evlog/telemetry/ingest'
import { describe, expect, it } from 'vitest'
import { parseAllowedCustomKeys, parseAllowedTools } from '../server/utils/allowed-tools'

const custom = {
ghaAction: 'evloghq/action',
ghaEvent: 'pull_request',
baselineMode: 'base',
checkOutcome: 'created',
commentOutcome: 'updated',
packages: 2,
entryPoints: 10,
score: 80,
instrumented: 6,
partial: 2,
dark: 2,
regressions: 1,
fixed: 3,
gatePassed: false,
baselineDelta: -5,
}

function body(fields: Record<string, string | number | boolean>) {
return JSON.stringify({ events: [
{
event: 'run',
command: 'map',
durationMs: 500,
outcome: 'success',
flags: { gate: false },
tool: { name: 'evlog-action', version: 'v1' },
env: { node: 'v24.0.0', ci: true, provider: 'github_actions', tty: false, agent: null, os: 'linux', arch: 'x64', environment: 'production' },
custom: fields,
idempotencyKey: 'action-run-test',
timestamp: '2026-10-05T12:00:00.000Z',
}
] })
}

const options = () => ({ allowedTools: parseAllowedTools(undefined), allowedCustomKeys: parseAllowedCustomKeys(undefined) })

describe('action telemetry ingestion', () => {
it('accepts action metadata, aggregate scan totals and reporting outcomes', () => {
const events = parseIngestBody(body(custom), options())
expect(events).toHaveLength(1)
expect(events[0]?.custom).toEqual(custom)
expect(events[0]?.tool).toEqual({ name: 'evlog-action', version: 'v1' })
})

it('drops repository data and undeclared fields', () => {
const events = parseIngestBody(body({ ...custom, repository: 'private/repo', sha: 'secret', token: 'secret', path: '/private/source.ts' }), options())
expect(events[0]?.custom).toEqual(custom)
})

it('keeps explicit deployment tool overrides authoritative', () => {
expect(() => parseIngestBody(body(custom), { ...options(), allowedTools: parseAllowedTools('evlog-cli') })).toThrow('unknown tool')
})
})
1 change: 1 addition & 0 deletions apps/telemetry/test/allowed-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('parseAllowedCustomKeys', () => {

it('lets an override replace the default keys for evlog-cli', () => {
expect(parseAllowedCustomKeys('{"evlog-cli":["onlyThis"]}')).toEqual({
...DEFAULT_ALLOWED_CUSTOM_KEYS,
'evlog-cli': ['onlyThis'],
})
})
Expand Down
10 changes: 9 additions & 1 deletion packages/telemetry/src/github-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ export function createGitHubActionsTelemetry<
const eventType = options.eventType ?? process.env.GITHUB_EVENT_NAME ?? 'unknown'

return {
...base,
get enabled() {
return base.enabled
},
set(fields) {
base.set(fields)
},
flush() {
return base.flush()
},
run(command, fn, opts) {
return base.run(command, fn, {
...opts,
Expand Down
69 changes: 69 additions & 0 deletions packages/telemetry/test/github-actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createGitHubActionsTelemetry } from '../src/github-actions'
import { disableTelemetry, enableTelemetry, _resetActiveTelemetryForTests } from '../src/create'
import type { RunEvent } from '../src/types'

const TOOL = 'github-actions-test'

describe('createGitHubActionsTelemetry', () => {
let directory: string
let events: RunEvent[]

beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'evlog-gha-test-'))
events = []
vi.stubEnv('XDG_CONFIG_HOME', directory)
vi.stubEnv('DO_NOT_TRACK', '0')
vi.stubEnv('EVLOG_TELEMETRY', '1')
vi.stubEnv('EVLOG_TELEMETRY_ENDPOINT', 'https://telemetry.test/ingest')
vi.stubEnv('GITHUB_ACTION', 'test-action')
vi.stubEnv('GITHUB_EVENT_NAME', 'push')
vi.stubGlobal('fetch', vi.fn((_url: string, init: RequestInit) => {
events.push(...JSON.parse(String(init.body)).events)
return Promise.resolve(new Response(null, { status: 204 }))
}))
})

afterEach(async () => {
_resetActiveTelemetryForTests()
vi.unstubAllGlobals()
vi.unstubAllEnvs()
await rm(directory, { recursive: true, force: true })
})

it('exposes the full handle and records custom fields with action metadata', async () => {
const handle = createGitHubActionsTelemetry({ name: TOOL, version: '1.0.0' })
expect(handle.enabled).toBe(true)
expect(typeof handle.set).toBe('function')
expect(typeof handle.flush).toBe('function')
await handle.flush()
const value = await handle.run('map', () => {
handle.set({ routes: 3 })
return 42
})
await handle.flush()
expect(value).toBe(42)
expect(events).toHaveLength(1)
expect(events[0]?.custom).toEqual({ routes: 3, ghaAction: 'test-action', ghaEvent: 'push' })
})

it('reflects consent changes through the enabled getter', async () => {
const handle = createGitHubActionsTelemetry({ name: TOOL, version: '1.0.0' })
await disableTelemetry(TOOL)
expect(handle.enabled).toBe(false)
await enableTelemetry(TOOL)
expect(handle.enabled).toBe(true)
})

it.each([{ DO_NOT_TRACK: '1' }, { EVLOG_TELEMETRY: '0' }])('runs work without delivery when opted out with %j', async (env) => {
for (const [key, value] of Object.entries(env)) vi.stubEnv(key, value)
const handle = createGitHubActionsTelemetry({ name: TOOL, version: '1.0.0' })
expect(handle.enabled).toBe(false)
expect(await handle.run('map', () => 42)).toBe(42)
await handle.flush()
expect(fetch).not.toHaveBeenCalled()
})
})
Loading