Commit 6c0666a
authored
feat: add anonymous PostHog telemetry (#342)
* feat: add anonymous PostHog telemetry
Anonymous, opt-out telemetry via PostHog (separate project from Framelink
SaaS). Captures tool_called events for get_figma_data, download_figma_images,
and the fetch CLI command, with payload size, node count, transport mode, and
sanitized error details.
- Random per-session UUID as distinct_id; PostHog GeoIP for rough unique-user
dedup. No persistent state, no credential touching.
- Disabled by --no-telemetry, FRAMELINK_TELEMETRY=off, or DO_NOT_TRACK=1.
- One-line stderr notice on every startup.
- Figma API key/OAuth token redacted from error messages before capture.
- Graceful flush on SIGINT + SIGTERM in both stdio and HTTP modes.
- Fetch CLI uses immediate flushing (flushAt:1, flushInterval:0) for the
short-lived process.
Bumps engines.node to >=20.0.0 (posthog-node requirement).
* refactor(telemetry): address review findings
- Discriminated union for ToolCallProperties (compile-time enforcement
of per-tool fields; output_format now only on get_figma_data events).
- Wrap PostHog capture/shutdown in try/catch — telemetry must never break
the tool handler's return value via a finally that throws.
- Tighten redaction min length to 8 chars to avoid garbling unrelated text
from accidental short secrets.
- Reset initialized=false in shutdown() so tests can re-init in the same
process.
- Extract shared get-figma-data pipeline (fetch → simplify → serialize →
measure) into src/services/get-figma-data.ts so the MCP tool and the
fetch CLI command don't drift. Tool uses progress hooks; CLI passes none.
- Wrap onShutdown() in try/finally inside registerShutdownHandlers so
process.exit(0) always runs.
* chore: tighten Node engines to >=20.20.0
posthog-node 5.x requires ^20.20.0 || >=22.22.0. Match the lower bound
honestly so install doesn't fail on stale Node 20 patches that the
previous >=20.0.0 incorrectly claimed to support.
* refactor(telemetry): move telemetry to edges via onComplete hooks
Domain functions (getFigmaData, downloadFigmaImages) no longer know about
telemetry. They fire a single onComplete lifecycle callback with timing +
metrics + error; the shell wires it to telemetry (or anything else).
- src/services/get-figma-data.ts: add GetFigmaDataOutcome type and
onComplete hook fired from a finally. Observer errors swallowed.
- src/services/download-figma-images.ts: NEW, extracts the download-plan
dedup logic + downloadImages call from the tool handler. Same
onComplete shape. Tool keeps param parsing, path validation, and
result formatting (edge concerns).
- src/services/telemetry.ts: unexport captureToolCall (now internal),
export captureGetFigmaDataCall / captureDownloadImagesCall that take
the outcome + a small { transport, authMode } context. The tool_called
event construction lives here in internal translator functions.
- src/mcp/tools/get-figma-data-tool.ts, download-figma-images-tool.ts,
src/commands/fetch.ts: strip all telemetry bookkeeping (let-bindings,
manual try/catch/finally). Shells are now thin — ~130 lines removed.
* refactor(telemetry): generic redactFromErrors, simpler init surface
- initTelemetry now takes { optOut, immediateFlush, redactFromErrors }.
The credential-named fields (figmaApiKey, figmaOAuthToken) are gone —
the shell just hands over any strings it wants scrubbed from error
messages, and telemetry filters out empties internally.
- initTelemetry returns boolean (resolved enabled state), letting
server.ts gate its disclosure notice without a separate getter.
- Move resolveTelemetryEnabled from config.ts into telemetry.ts where
it belongs. config.ts imports it for the printout.
- Drop ServerConfig.telemetryEnabled in favor of passing the raw
noTelemetry flag through config. Telemetry owns resolution.
- Drop the >= 8 length heuristic — the shell is the right place to
decide what counts as a real secret, not telemetry.
* feat(telemetry): richer design-shape metrics on get_figma_data
Adds style_count, component_count, instance_count, text_node_count,
image_node_count, component_property_count, and has_variables to the
tool_called event. All computed in one extra walk of the simplified
tree plus an early-exiting walk of the raw response for variable
detection. Also splits the old node_count into raw/simplified/max_depth
and renames node_major -> nodejs_major for clarity.
* feat(telemetry): replace style_count with named_style_count
The old style_count was counting entries in globalVars.styles — our
internal dedup cache, not Figma's named/published styles. That's
redundant with the size metrics for "how visually varied is this file."
What we actually want is a signal for design-system maturity: how
many reusable named styles (from the Figma Styles panel) does this
file reference. Read directly from the raw API response's top-level
styles dict (or merged across per-node entries for GetFileNodesResponse).
* refactor: update imports for telemetry/error-meta file reorganization
Point all consumers at new locations after file moves:
- ~/services/telemetry.js -> ~/telemetry/index.js
- ~/services/error-meta.js -> ~/utils/error-meta.js
- Metric helpers extracted from get-figma-data.ts to get-figma-data-metrics.ts
- Validation capture extracted from mcp/index.ts to mcp/validation-capture.ts
- Add 429 rate-limit user-facing error in figma.ts
- Delete old src/services/telemetry.ts
* feat(telemetry): error taxonomy, phase timings, client identity, validation capture
Structured error analytics:
- tagError/getErrorMeta in utils/error-meta.ts — attaches phase, http_status,
network_code, is_retryable to thrown errors via a Symbol key; walks the cause
chain so wrappers preserve inner metadata
- fetch_ms/simplify_ms/serialize_ms phase timings on get_figma_data events
- client_name/client_version from MCP initialize handshake (stdio reliable,
stateless HTTP best-effort)
- Validation reject capture via McpServer.validateToolInput monkey patch with
structured validation_field/validation_rule; also captures path-traversal
rejects from the download tool
- 2000-char error_message truncation
- Figma 429 rate limit message with plan-specific guidance
Bug fixes:
- Race-prone nodesProcessed module-global replaced with per-call NodeCounter
on TraversalContext — concurrent HTTP requests no longer corrupt each other
- Opt-out init no longer poisons subsequent re-init attempts
- "Anonymous telemetry" banner updated to "Usage telemetry" (GeoIP is on)
File reorganization:
- src/telemetry/{client,capture,types,index}.ts split from services/telemetry.ts
- src/services/get-figma-data-metrics.ts extracted from get-figma-data.ts
- src/mcp/validation-capture.ts extracted from mcp/index.ts
- src/utils/error-meta.ts moved from services/
* fix(telemetry): redact before truncate, dynamic 429 guidance, pin MCP SDK
- P3 fix: move error_message truncation to client.ts so it runs AFTER secret
redaction — a token straddling the 2000-char cutoff can no longer survive
as a partial match.
- 429 message now reads Figma's rate-limit response headers (Retry-After,
X-Figma-Plan-Tier, X-Figma-Rate-Limit-Type, X-Figma-Upgrade-Link) and
gives targeted guidance based on actual seat type and plan tier, instead
of generic "free plan → upgrade" advice.
- Pin @modelcontextprotocol/sdk to exact 1.27.1 (was ^1.27.1). The
validation-capture monkey patch targets a private SDK method that won't
surface as a type error if renamed — tests are the only safety net, so
SDK upgrades should be deliberate.
* chore: upgrade @modelcontextprotocol/sdk to 1.29.0
Upgraded from 1.27.1. The validateToolInput monkey patch still works —
validation-reject integration tests pass, confirming the private method
signature hasn't changed in this release.1 parent b1ee6a4 commit 6c0666a
28 files changed
Lines changed: 1857 additions & 281 deletions
File tree
- src
- commands
- extractors
- mcp
- tools
- services
- telemetry
- tests
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
53 | 57 | | |
54 | 58 | | |
55 | 59 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
46 | 51 | | |
47 | 52 | | |
48 | 53 | | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
53 | 60 | | |
54 | 61 | | |
55 | 62 | | |
| |||
62 | 69 | | |
63 | 70 | | |
64 | 71 | | |
| 72 | + | |
65 | 73 | | |
66 | 74 | | |
67 | 75 | | |
| |||
87 | 95 | | |
88 | 96 | | |
89 | 97 | | |
90 | | - | |
91 | 98 | | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
100 | 105 | | |
101 | 106 | | |
102 | | - | |
103 | | - | |
104 | | - | |
| 107 | + | |
105 | 108 | | |
106 | | - | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
107 | 119 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| 36 | + | |
34 | 37 | | |
35 | 38 | | |
36 | 39 | | |
| |||
134 | 137 | | |
135 | 138 | | |
136 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
137 | 148 | | |
138 | 149 | | |
139 | 150 | | |
| |||
144 | 155 | | |
145 | 156 | | |
146 | 157 | | |
| 158 | + | |
147 | 159 | | |
148 | 160 | | |
149 | 161 | | |
| |||
168 | 180 | | |
169 | 181 | | |
170 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
171 | 187 | | |
172 | 188 | | |
173 | 189 | | |
| |||
180 | 196 | | |
181 | 197 | | |
182 | 198 | | |
| 199 | + | |
183 | 200 | | |
184 | 201 | | |
185 | 202 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | | - | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
0 commit comments