|
| 1 | +# 1DS Telemetry — A Walkthrough for Plugin Owners |
| 2 | + |
| 3 | +**Date:** 2026-04-29 · **Author:** Amit Joshi |
| 4 | +**For:** Folks owning `canvas-apps`, `code-apps`, `mcp-apps`, `model-apps` |
| 5 | +**Status:** Work in progress. The cluster currently wired up is a **testing-only** iKey we used to validate the pipeline end-to-end. The plan is for **each plugin to configure its own cluster** before any real adoption. |
| 6 | +**Hope:** Share what we built for `power-pages`, hear what you'd want different, and make adoption easy if you're up for it. |
| 7 | +**Engineering critique companion:** `2026-04-27-1ds-telemetry-team-presentation.md` |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Agenda |
| 12 | + |
| 13 | +| § | Topic | Time | |
| 14 | +|---|---|---| |
| 15 | +| 1 | What we built | 2 min | |
| 16 | +| 2 | A few decisions worth flagging | 5 min | |
| 17 | +| 3 | What's shared vs what would live in your plugin | 2 min | |
| 18 | +| 4 | If you'd like to adopt — a suggested path | 4 min | |
| 19 | +| 5 | How we've been verifying things land | 1 min | |
| 20 | +| 6 | Open questions and likely concerns | 1 min | |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## 1. What we built |
| 25 | + |
| 26 | +- A **shared, Node-only** telemetry library at `shared/telemetry/`. |
| 27 | +- **Zero npm dependencies** — built on `https`, `child_process`, `fs`, `crypto`. We wanted to avoid adding an install step to any plugin. |
| 28 | +- Four lifecycle events to **Microsoft 1DS / OneCollector**: |
| 29 | + - `skill_started` / `skill_completed` |
| 30 | + - `script_started` / `script_completed` |
| 31 | +- Today, events land in Kusto table **`PowerPlatformExtensionEvent`** via a **testing-only iKey** (routing tuple: `iKey` + `envelope.name="VscodeEvent"`). Per-plugin clusters are the next step — the test cluster was just to prove the pipeline works. |
| 32 | +- **Default-on with opt-out.** No first-run prompt — happy to revisit if any of you feel differently. |
| 33 | +- **Fail-closed.** A detached child owns the POST so the parent never blocks. |
| 34 | +- First adopter is **`power-pages`**, and Kusto landing is verified end-to-end. |
| 35 | + |
| 36 | +> The hope is that you mostly **inherit configuration knobs**, not code. |
| 37 | +
|
| 38 | +--- |
| 39 | + |
| 40 | +## 2. A few decisions worth flagging |
| 41 | + |
| 42 | +These are choices we landed on for `power-pages`. Each comes with a tradeoff. If any of them feel wrong for your plugin, that's exactly the kind of feedback we'd love before you adopt. |
| 43 | + |
| 44 | +### 2.1 Raw `node:https` instead of the `@microsoft/1ds-*` SDK |
| 45 | + |
| 46 | +- **Reasoning.** We wanted to keep marketplace plugins free of `npm install`. A POC showed identical Kusto landing either way. |
| 47 | +- **Tradeoff.** When 1DS evolves the wire format, we'll own the migration ourselves. The surface is small — one envelope builder. |
| 48 | + |
| 49 | +### 2.2 Detached child dispatcher instead of an inline POST |
| 50 | + |
| 51 | +- **Reasoning.** Hooks have a 30 s timeout and sit on the user's critical path. Detached spawn returns in **~50 ms** regardless of network conditions, which felt important for UX. |
| 52 | +- **Tradeoff.** If a process supervisor or antivirus kills the child early, the event is silently dropped. We chose simplicity over a retry queue, but we're open to revisiting if your environment makes drops more common. |
| 53 | + |
| 54 | +### 2.3 Strict allowlist instead of a runtime PII scrubber |
| 55 | + |
| 56 | +- **Reasoning.** Allowlists fail safe — a forgotten field doesn't ship. Scrubber regexes can fail open. The allowlist sits in one place (`lib/events.js`) and CI asserts the keyset. |
| 57 | +- **Tradeoff.** Adding a new field means editing the builder *and* updating the privacy reference doc. We've found this friction useful, but we'd love to know if it gets in your way. |
| 58 | + |
| 59 | +### 2.4 Default-on with opt-out, not an interactive prompt |
| 60 | + |
| 61 | +- **Reasoning.** An earlier iteration prompted on first run. It cost the very first invocation per machine and added friction users didn't ask for. Given the allowlist already prevents PII, we felt default-on was defensible. |
| 62 | +- **What this means for you.** You wouldn't need to add a Phase-1 consent block to your skills. If your plugin's audience expects an opt-in posture, please flag it — we can talk through it. |
| 63 | + |
| 64 | +### 2.5 Sync script instead of git submodule or npm package |
| 65 | + |
| 66 | +- **Reasoning.** Submodules can break for users who clone non-recursively, and a private npm package would force an install step. The sync script is ~30 lines and produces a self-contained plugin. |
| 67 | +- **Tradeoff.** Drift if someone hand-edits the synced copy. Convention so far: edit `shared/`, re-run sync, never touch the synced files directly. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## 3. What's shared vs what would live in your plugin |
| 72 | + |
| 73 | +### 3.1 Shared (we'd ask you not to edit these in your plugin) |
| 74 | + |
| 75 | +| Thing | Where | |
| 76 | +|---|---| |
| 77 | +| Library (13 files) | `shared/telemetry/lib/` | |
| 78 | +| Privacy / opt-out doc | `shared/telemetry/references/telemetry-consent-reference.md` | |
| 79 | +| Sync tool | `shared/telemetry/sync-to-plugin.js` | |
| 80 | +| Opt-out env var name | `POWER_PLATFORM_SKILLS_TELEMETRY` | |
| 81 | +| Consent file path | `~/.power-platform-skills/telemetry.json` | |
| 82 | + |
| 83 | +### 3.2 Yours to configure |
| 84 | + |
| 85 | +| Thing | What you'd provide | |
| 86 | +|---|---| |
| 87 | +| iKey + collector URL | Your own — provisioned per plugin, dropped into the synced `scripts/lib/telemetry/ikey.json` | |
| 88 | +| Plugin name | A string literal in your hooks + telemetry-runner | |
| 89 | +| Plugin version | Already in `.claude-plugin/plugin.json` — hooks just read it | |
| 90 | +| Tracked skills | A `{ skill-name: { validatorScript } }` map | |
| 91 | +| Hook entry points | Three thin wrappers (~80 lines each) | |
| 92 | +| `withTelemetry` adoption | Wrap whichever Node scripts you want instrumented | |
| 93 | + |
| 94 | +> **iKey ownership.** The current `shared/telemetry/ikey.json` carries our **testing iKey** — handy for proving the pipeline lands data in Kusto, but not what anyone should ship with. Before you adopt for real, we'd suggest provisioning your own iKey + Kusto stream so your data and dashboards stay yours. We can help walk through the tenant-side setup if it's new ground. |
| 95 | +
|
| 96 | +--- |
| 97 | + |
| 98 | +## 4. If you'd like to adopt — a suggested path |
| 99 | + |
| 100 | +We've ballparked this at ~30 minutes for a plugin that already has hooks. Happy to pair on the first one with whoever wants to try. |
| 101 | + |
| 102 | +### Step 1 — Sync the library |
| 103 | + |
| 104 | +```bash |
| 105 | +node shared/telemetry/sync-to-plugin.js --target plugins/<your-plugin> |
| 106 | +``` |
| 107 | + |
| 108 | +You'll get: |
| 109 | + |
| 110 | +``` |
| 111 | +plugins/<your-plugin>/ |
| 112 | +├── scripts/lib/telemetry/ |
| 113 | +│ ├── ikey.json |
| 114 | +│ └── lib/ # 13 files |
| 115 | +└── references/telemetry-consent-reference.md |
| 116 | +``` |
| 117 | + |
| 118 | +Worth noting in your CLAUDE.md / AGENTS.md: the synced copy is generated — edits should go in `shared/`, then a re-sync. |
| 119 | + |
| 120 | +> **Heads-up on `ikey.json`.** The synced file initially carries our testing iKey. Before you ship anything, replace it with the iKey + collector URL for the cluster your team owns. (We'll likely add a `--ikey` flag to the sync script to make this less manual — happy to take input on the shape.) |
| 121 | +
|
| 122 | +### Step 2 — Define your tracked skills |
| 123 | + |
| 124 | +Somewhere like `scripts/lib/<your-plugin>-hook-utils.js`: |
| 125 | + |
| 126 | +```js |
| 127 | +const TRACKED_SKILLS = { |
| 128 | + "create-something": { validatorScript: "scripts/validators/create-something.js" }, |
| 129 | + "deploy-something": { validatorScript: "scripts/validators/deploy-something.js" }, |
| 130 | +}; |
| 131 | + |
| 132 | +function getTrackedSkillFromToolInput(toolInput) { |
| 133 | + // see plugins/power-pages/scripts/lib/powerpages-hook-utils.js for a reference |
| 134 | +} |
| 135 | + |
| 136 | +module.exports = { TRACKED_SKILLS, getTrackedSkillFromToolInput }; |
| 137 | +``` |
| 138 | + |
| 139 | +> **A small gotcha we hit:** keys are skill names **without** the plugin prefix — `create-site`, not `power-pages:create-site`. The slash-command detector adds the prefix at match time. |
| 140 | +
|
| 141 | +### Step 3 — Wire the three hooks |
| 142 | + |
| 143 | +Easiest start is to copy from `plugins/power-pages/hooks/` and adjust two things per file: |
| 144 | + |
| 145 | +| File | Change 1 | Change 2 | |
| 146 | +|---|---|---| |
| 147 | +| `hooks.json` | (no change needed) | (no change needed) | |
| 148 | +| `run-skill-pretool-telemetry.js` | `plugin_name: "<your-plugin>"` | swap the hook-utils require | |
| 149 | +| `run-skill-posttool-validation.js` | same | same | |
| 150 | +| `run-user-prompt-telemetry.js` | `pluginName: "<your-plugin>"` | swap the hook-utils require | |
| 151 | + |
| 152 | +### Step 4 — Optional: instrument scripts with `withTelemetry` |
| 153 | + |
| 154 | +If there are Node scripts in your plugin you'd like signal on, copy `plugins/power-pages/scripts/lib/telemetry-runner.js` (changing the plugin name string), and then: |
| 155 | + |
| 156 | +```js |
| 157 | +const { runInstrumented } = require("./lib/telemetry-runner"); |
| 158 | + |
| 159 | +(async () => { |
| 160 | + await runInstrumented("deploy-something", async () => { |
| 161 | + // your existing script body |
| 162 | + }); |
| 163 | +})(); |
| 164 | +``` |
| 165 | + |
| 166 | +`outcome` is derived from whether the function throws, and the original error is rethrown unchanged. |
| 167 | + |
| 168 | +### Step 5 — Mention the opt-out in your README |
| 169 | + |
| 170 | +Something like: |
| 171 | + |
| 172 | +``` |
| 173 | +Anonymous telemetry is enabled by default. See |
| 174 | +references/telemetry-consent-reference.md for details and opt-out instructions. |
| 175 | +``` |
| 176 | + |
| 177 | +Linking the synced doc keeps you in sync with future updates without you having to track them. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## 5. How we've been verifying things land |
| 182 | + |
| 183 | +Two stages — local first, Kusto second. |
| 184 | + |
| 185 | +### Local — placeholder iKey |
| 186 | + |
| 187 | +If you sync without provisioning a real iKey, the dispatcher writes events to `~/.power-platform-skills/events.jsonl`. Run a tracked skill, then: |
| 188 | + |
| 189 | +```bash |
| 190 | +tail -n 5 ~/.power-platform-skills/events.jsonl | jq . |
| 191 | +``` |
| 192 | + |
| 193 | +You should see your `plugin_name` and the right `skill_name`. |
| 194 | + |
| 195 | +### Kusto — real iKey |
| 196 | + |
| 197 | +The query below is what we ran against the testing cluster (table `PowerPlatformExtensionEvent`). Once you're on your own cluster, swap in your table name — the `EventInfo` shape stays the same. |
| 198 | + |
| 199 | +```kusto |
| 200 | +PowerPlatformExtensionEvent // ← your table name once you're on your own cluster |
| 201 | +| where TimeGenerated > ago(15m) |
| 202 | +| extend info = parse_json(EventInfo) |
| 203 | +| where tostring(info.plugin_name) == "<your-plugin>" |
| 204 | +| project TimeGenerated, EventName, |
| 205 | + plugin = tostring(info.plugin_name), |
| 206 | + skill = tostring(info.skill_name), |
| 207 | + outcome = tostring(info.outcome) |
| 208 | +| order by TimeGenerated desc |
| 209 | +``` |
| 210 | + |
| 211 | +A couple of things to look for: |
| 212 | + |
| 213 | +1. `EventName` matches one of the four event types. |
| 214 | +2. `correlation_id` matches between `_started` and `_completed`. |
| 215 | + |
| 216 | +> **Something we learned the hard way:** `acc:1` from OneCollector is wire-layer ack only — it doesn't mean ingestion succeeded. The Kusto query above is the real check. |
| 217 | +
|
| 218 | +--- |
| 219 | + |
| 220 | +## 6. Open questions and likely concerns |
| 221 | + |
| 222 | +We'd genuinely like input on these. |
| 223 | + |
| 224 | +**Could I add a custom field?** |
| 225 | +Definitely possible — the path is `shared/telemetry/lib/events.js` (allowlist) + the privacy doc + a CI test asserting the new keyset. We've kept the surface small on purpose; if you have fields in mind, let's talk through them and add together. |
| 226 | + |
| 227 | +**My plugin would prefer its own Kusto stream / dashboard owner.** |
| 228 | +That's exactly the direction we're heading. The current shared iKey is just a testing setup we used to validate the pipeline; **per-plugin clusters are the planned default**. Practically that means each plugin provisions its own iKey + tenant annotation and drops the values into the synced `ikey.json`. Happy to walk through the tenant-side bits with anyone for whom this is new. |
| 229 | + |
| 230 | +**Will this affect my existing PostToolUse validator?** |
| 231 | +It shouldn't. In `power-pages`, telemetry was folded around the existing validator, and the validator's exit code is preserved. If your validator setup looks different, happy to walk through it together. |
| 232 | + |
| 233 | +**What about `--plugin-dir` dev mode?** |
| 234 | +Worth flagging: hooks don't register under `--plugin-dir` (Claude Code limitation). The slash-command path covers user-typed `/plugin:skill` invocations, but auto-invoked skills in dev mode aren't captured. End-to-end verification needs a marketplace install. |
| 235 | + |
| 236 | +**Why one consent file across all plugins, not per-plugin?** |
| 237 | +Our intuition was that users think of this as "Power Platform Skills telemetry" rather than per-plugin telemetry, so a single opt-out covers everything. If your audience would expect per-plugin consent, we'd love to hear that — it's not a hard call to revisit. |
| 238 | + |
| 239 | +--- |
| 240 | + |
| 241 | +## 7. References |
| 242 | + |
| 243 | +| Doc | What it's for | |
| 244 | +|---|---| |
| 245 | +| `2026-04-20-1ds-telemetry-design.md` | Full internal design spec | |
| 246 | +| `2026-04-27-1ds-telemetry-team-presentation.md` | Engineering critique companion | |
| 247 | +| `shared/telemetry/README.md` | Field reference + sync command | |
| 248 | +| `shared/telemetry/references/telemetry-consent-reference.md` | What's sent, what isn't, how to opt out | |
| 249 | +| `plugins/power-pages/hooks/` | Reference impl: all three hooks | |
| 250 | +| `plugins/power-pages/scripts/lib/telemetry-runner.js` | Reference impl: `withTelemetry` shim | |
| 251 | + |
| 252 | +> If anything here contradicts the code, the code is the source of truth — please flag it and we'll update the doc. |
0 commit comments