@hyperdrive.bot/plugin-telemetry 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/run.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -0,0 +1,54 @@
1
+ // src/config.ts
2
+ import { createHash } from "crypto";
3
+ import { hostname, userInfo } from "os";
4
+ var GA4_MEASUREMENT_ID = true ? "__GA4_MEASUREMENT_ID__" : "__GA4_MEASUREMENT_ID__";
5
+ var GA4_API_SECRET = true ? "__GA4_API_SECRET__" : "__GA4_API_SECRET__";
6
+ var GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
7
+ function isConfigured() {
8
+ return typeof GA4_MEASUREMENT_ID === "string" && typeof GA4_API_SECRET === "string" && !GA4_MEASUREMENT_ID.startsWith("__") && !GA4_MEASUREMENT_ID.endsWith("__") && !GA4_API_SECRET.startsWith("__") && !GA4_API_SECRET.endsWith("__") && GA4_MEASUREMENT_ID.length > 0 && GA4_API_SECRET.length > 0;
9
+ }
10
+ function getPlatformGA4Config() {
11
+ return {
12
+ measurementId: GA4_MEASUREMENT_ID,
13
+ apiSecret: GA4_API_SECRET,
14
+ isConfigured: isConfigured()
15
+ };
16
+ }
17
+ function getClientId() {
18
+ const machineIdentifier = `${hostname()}:${userInfo().username}`;
19
+ const hash = createHash("sha256").update(machineIdentifier).digest("hex");
20
+ return hash.slice(0, 32);
21
+ }
22
+
23
+ // src/ga4-client.ts
24
+ function sendEvent(event) {
25
+ if (!isConfigured()) return;
26
+ const url = `${GA4_ENDPOINT}?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
27
+ const body = JSON.stringify({
28
+ client_id: getClientId(),
29
+ events: [event]
30
+ });
31
+ const controller = new AbortController();
32
+ const timeout = setTimeout(() => controller.abort(), 5e3);
33
+ fetch(url, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body,
37
+ signal: controller.signal
38
+ }).then(() => {
39
+ clearTimeout(timeout);
40
+ }).catch(() => {
41
+ clearTimeout(timeout);
42
+ });
43
+ }
44
+
45
+ export {
46
+ GA4_MEASUREMENT_ID,
47
+ GA4_API_SECRET,
48
+ GA4_ENDPOINT,
49
+ isConfigured,
50
+ getPlatformGA4Config,
51
+ getClientId,
52
+ sendEvent
53
+ };
54
+ //# sourceMappingURL=chunk-YMQEC245.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/ga4-client.ts"],"sourcesContent":["import {createHash} from 'node:crypto'\nimport {hostname, userInfo} from 'node:os'\n\n/**\n * Build-time credential placeholders — esbuild `define` replaces these identifiers.\n * The typeof guard provides safe fallback when running via ts-node (tests).\n */\ndeclare const __GA4_MEASUREMENT_ID__: string\ndeclare const __GA4_API_SECRET__: string\n\n/**\n * GA4 Measurement ID — build-time placeholder.\n * CI replaces this value via tsup/esbuild define before publish.\n * When still set to the placeholder, sendEvent() no-ops.\n */\nexport const GA4_MEASUREMENT_ID: string =\n typeof __GA4_MEASUREMENT_ID__ !== 'undefined' ? __GA4_MEASUREMENT_ID__ : '__GA4_MEASUREMENT_ID__'\n\n/**\n * GA4 API Secret — build-time placeholder.\n * CI replaces this value via tsup/esbuild define before publish.\n * When still set to the placeholder, sendEvent() no-ops.\n */\nexport const GA4_API_SECRET: string =\n typeof __GA4_API_SECRET__ !== 'undefined' ? __GA4_API_SECRET__ : '__GA4_API_SECRET__'\n\n/**\n * GA4 Measurement Protocol endpoint\n */\nexport const GA4_ENDPOINT = 'https://www.google-analytics.com/mp/collect'\n\n/**\n * Check if GA4 credentials have been injected at build time.\n * Returns false when placeholders are still present (local dev without env vars).\n */\nexport function isConfigured(): boolean {\n return (\n typeof GA4_MEASUREMENT_ID === 'string' &&\n typeof GA4_API_SECRET === 'string' &&\n !GA4_MEASUREMENT_ID.startsWith('__') &&\n !GA4_MEASUREMENT_ID.endsWith('__') &&\n !GA4_API_SECRET.startsWith('__') &&\n !GA4_API_SECRET.endsWith('__') &&\n GA4_MEASUREMENT_ID.length > 0 &&\n GA4_API_SECRET.length > 0\n )\n}\n\n/**\n * Get the full GA4 platform configuration.\n *\n * @returns Object with measurementId, apiSecret, and isConfigured flag\n */\nexport function getPlatformGA4Config(): {measurementId: string; apiSecret: string; isConfigured: boolean} {\n return {\n measurementId: GA4_MEASUREMENT_ID,\n apiSecret: GA4_API_SECRET,\n isConfigured: isConfigured(),\n }\n}\n\n/**\n * Generate a stable, anonymous client ID from the machine's hostname and username.\n * Returns 32 hex characters (truncated SHA-256 hash).\n * This provides a consistent identifier per machine without leaking PII.\n */\nexport function getClientId(): string {\n const machineIdentifier = `${hostname()}:${userInfo().username}`\n const hash = createHash('sha256').update(machineIdentifier).digest('hex')\n return hash.slice(0, 32)\n}\n","import {GA4_MEASUREMENT_ID, GA4_API_SECRET, GA4_ENDPOINT, isConfigured, getClientId} from './config.js'\n\n/**\n * GA4 event structure for the Measurement Protocol\n */\nexport interface GA4Event {\n name: string\n params: Record<string, string | number>\n}\n\n/**\n * Send an event to GA4 via the Measurement Protocol.\n *\n * Fire-and-forget: this function returns void and never throws.\n * The HTTP request is not awaited — it runs in the background.\n * All errors (network, timeout, non-2xx) are silently swallowed.\n *\n * No-ops when GA4 credentials are still build-time placeholders.\n */\nexport function sendEvent(event: GA4Event): void {\n if (!isConfigured()) return\n\n const url = `${GA4_ENDPOINT}?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`\n\n const body = JSON.stringify({\n client_id: getClientId(),\n events: [event],\n })\n\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), 5000)\n\n fetch(url, {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body,\n signal: controller.signal,\n })\n .then(() => {\n clearTimeout(timeout)\n })\n .catch(() => {\n clearTimeout(timeout)\n // Silently swallow all errors — fire-and-forget\n })\n}\n"],"mappings":";AAAA,SAAQ,kBAAiB;AACzB,SAAQ,UAAU,gBAAe;AAc1B,IAAM,qBACX,OAAgD,2BAAyB;AAOpE,IAAM,iBACX,OAA4C,uBAAqB;AAK5D,IAAM,eAAe;AAMrB,SAAS,eAAwB;AACtC,SACE,OAAO,uBAAuB,YAC9B,OAAO,mBAAmB,YAC1B,CAAC,mBAAmB,WAAW,IAAI,KACnC,CAAC,mBAAmB,SAAS,IAAI,KACjC,CAAC,eAAe,WAAW,IAAI,KAC/B,CAAC,eAAe,SAAS,IAAI,KAC7B,mBAAmB,SAAS,KAC5B,eAAe,SAAS;AAE5B;AAOO,SAAS,uBAA0F;AACxG,SAAO;AAAA,IACL,eAAe;AAAA,IACf,WAAW;AAAA,IACX,cAAc,aAAa;AAAA,EAC7B;AACF;AAOO,SAAS,cAAsB;AACpC,QAAM,oBAAoB,GAAG,SAAS,CAAC,IAAI,SAAS,EAAE,QAAQ;AAC9D,QAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,iBAAiB,EAAE,OAAO,KAAK;AACxE,SAAO,KAAK,MAAM,GAAG,EAAE;AACzB;;;ACnDO,SAAS,UAAU,OAAuB;AAC/C,MAAI,CAAC,aAAa,EAAG;AAErB,QAAM,MAAM,GAAG,YAAY,mBAAmB,kBAAkB,eAAe,cAAc;AAE7F,QAAM,OAAO,KAAK,UAAU;AAAA,IAC1B,WAAW,YAAY;AAAA,IACvB,QAAQ,CAAC,KAAK;AAAA,EAChB,CAAC;AAED,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AAEzD,QAAM,KAAK;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,EAAC,gBAAgB,mBAAkB;AAAA,IAC5C;AAAA,IACA,QAAQ,WAAW;AAAA,EACrB,CAAC,EACE,KAAK,MAAM;AACV,iBAAa,OAAO;AAAA,EACtB,CAAC,EACA,MAAM,MAAM;AACX,iBAAa,OAAO;AAAA,EAEtB,CAAC;AACL;","names":[]}
@@ -0,0 +1,12 @@
1
+ import { Hook } from '@oclif/core';
2
+
3
+ /**
4
+ * oclif postrun hook — emits cli_command or cli_command_error GA4 events
5
+ * after every CLI command execution.
6
+ *
7
+ * Fire-and-forget: sendEvent() is called without await.
8
+ * The entire body is wrapped in try/catch to never propagate errors to oclif.
9
+ */
10
+ declare const hook: Hook.Postrun;
11
+
12
+ export { hook as default };
@@ -0,0 +1,37 @@
1
+ import {
2
+ sendEvent
3
+ } from "../chunk-YMQEC245.js";
4
+
5
+ // src/hooks/postrun.ts
6
+ var hook = async function(options) {
7
+ try {
8
+ const command = options.Command?.id ?? "unknown";
9
+ const cli = options.config?.pjson?.name ?? "unknown";
10
+ const cliVersion = options.config?.pjson?.version ?? "unknown";
11
+ const plugin = options.Command?.plugin?.name ?? cli;
12
+ const os = process.platform;
13
+ const nodeVersion = process.version;
14
+ const params = {
15
+ command,
16
+ cli,
17
+ cli_version: cliVersion,
18
+ plugin,
19
+ os,
20
+ node_version: nodeVersion
21
+ };
22
+ if (options.error) {
23
+ const error = options.error;
24
+ const errorType = error.constructor?.name || "unknown";
25
+ params.error_type = errorType.slice(0, 100);
26
+ sendEvent({ name: "cli_command_error", params });
27
+ } else {
28
+ sendEvent({ name: "cli_command", params });
29
+ }
30
+ } catch {
31
+ }
32
+ };
33
+ var postrun_default = hook;
34
+ export {
35
+ postrun_default as default
36
+ };
37
+ //# sourceMappingURL=postrun.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/postrun.ts"],"sourcesContent":["import type {Hook} from '@oclif/core'\n\nimport {sendEvent} from '../ga4-client.js'\n\n/**\n * oclif postrun hook — emits cli_command or cli_command_error GA4 events\n * after every CLI command execution.\n *\n * Fire-and-forget: sendEvent() is called without await.\n * The entire body is wrapped in try/catch to never propagate errors to oclif.\n */\nconst hook: Hook.Postrun = async function (options) {\n try {\n const command = options.Command?.id ?? 'unknown'\n const cli = options.config?.pjson?.name ?? 'unknown'\n const cliVersion = options.config?.pjson?.version ?? 'unknown'\n const plugin = options.Command?.plugin?.name ?? cli\n const os = process.platform\n const nodeVersion = process.version\n\n const params: Record<string, string> = {\n command,\n cli,\n cli_version: cliVersion,\n plugin,\n os,\n node_version: nodeVersion,\n }\n\n if ((options as {error?: Error}).error) {\n const error = (options as {error?: Error}).error!\n const errorType = error.constructor?.name || 'unknown'\n params.error_type = errorType.slice(0, 100)\n sendEvent({name: 'cli_command_error', params})\n } else {\n sendEvent({name: 'cli_command', params})\n }\n } catch {\n // Silently swallow all errors — the hook must never throw\n }\n}\n\nexport default hook\n"],"mappings":";;;;;AAWA,IAAM,OAAqB,eAAgB,SAAS;AAClD,MAAI;AACF,UAAM,UAAU,QAAQ,SAAS,MAAM;AACvC,UAAM,MAAM,QAAQ,QAAQ,OAAO,QAAQ;AAC3C,UAAM,aAAa,QAAQ,QAAQ,OAAO,WAAW;AACrD,UAAM,SAAS,QAAQ,SAAS,QAAQ,QAAQ;AAChD,UAAM,KAAK,QAAQ;AACnB,UAAM,cAAc,QAAQ;AAE5B,UAAM,SAAiC;AAAA,MACrC;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA,cAAc;AAAA,IAChB;AAEA,QAAK,QAA4B,OAAO;AACtC,YAAM,QAAS,QAA4B;AAC3C,YAAM,YAAY,MAAM,aAAa,QAAQ;AAC7C,aAAO,aAAa,UAAU,MAAM,GAAG,GAAG;AAC1C,gBAAU,EAAC,MAAM,qBAAqB,OAAM,CAAC;AAAA,IAC/C,OAAO;AACL,gBAAU,EAAC,MAAM,eAAe,OAAM,CAAC;AAAA,IACzC;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,IAAO,kBAAQ;","names":[]}
@@ -0,0 +1,59 @@
1
+ export { run } from '@oclif/core';
2
+
3
+ /**
4
+ * GA4 event structure for the Measurement Protocol
5
+ */
6
+ interface GA4Event {
7
+ name: string;
8
+ params: Record<string, string | number>;
9
+ }
10
+ /**
11
+ * Send an event to GA4 via the Measurement Protocol.
12
+ *
13
+ * Fire-and-forget: this function returns void and never throws.
14
+ * The HTTP request is not awaited — it runs in the background.
15
+ * All errors (network, timeout, non-2xx) are silently swallowed.
16
+ *
17
+ * No-ops when GA4 credentials are still build-time placeholders.
18
+ */
19
+ declare function sendEvent(event: GA4Event): void;
20
+
21
+ /**
22
+ * GA4 Measurement ID — build-time placeholder.
23
+ * CI replaces this value via tsup/esbuild define before publish.
24
+ * When still set to the placeholder, sendEvent() no-ops.
25
+ */
26
+ declare const GA4_MEASUREMENT_ID: string;
27
+ /**
28
+ * GA4 API Secret — build-time placeholder.
29
+ * CI replaces this value via tsup/esbuild define before publish.
30
+ * When still set to the placeholder, sendEvent() no-ops.
31
+ */
32
+ declare const GA4_API_SECRET: string;
33
+ /**
34
+ * GA4 Measurement Protocol endpoint
35
+ */
36
+ declare const GA4_ENDPOINT = "https://www.google-analytics.com/mp/collect";
37
+ /**
38
+ * Check if GA4 credentials have been injected at build time.
39
+ * Returns false when placeholders are still present (local dev without env vars).
40
+ */
41
+ declare function isConfigured(): boolean;
42
+ /**
43
+ * Get the full GA4 platform configuration.
44
+ *
45
+ * @returns Object with measurementId, apiSecret, and isConfigured flag
46
+ */
47
+ declare function getPlatformGA4Config(): {
48
+ measurementId: string;
49
+ apiSecret: string;
50
+ isConfigured: boolean;
51
+ };
52
+ /**
53
+ * Generate a stable, anonymous client ID from the machine's hostname and username.
54
+ * Returns 32 hex characters (truncated SHA-256 hash).
55
+ * This provides a consistent identifier per machine without leaking PII.
56
+ */
57
+ declare function getClientId(): string;
58
+
59
+ export { type GA4Event, GA4_API_SECRET, GA4_ENDPOINT, GA4_MEASUREMENT_ID, getClientId, getPlatformGA4Config, isConfigured, sendEvent };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ GA4_API_SECRET,
3
+ GA4_ENDPOINT,
4
+ GA4_MEASUREMENT_ID,
5
+ getClientId,
6
+ getPlatformGA4Config,
7
+ isConfigured,
8
+ sendEvent
9
+ } from "./chunk-YMQEC245.js";
10
+
11
+ // src/index.ts
12
+ import { run } from "@oclif/core";
13
+ export {
14
+ GA4_API_SECRET,
15
+ GA4_ENDPOINT,
16
+ GA4_MEASUREMENT_ID,
17
+ getClientId,
18
+ getPlatformGA4Config,
19
+ isConfigured,
20
+ run,
21
+ sendEvent
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {run} from '@oclif/core'\nexport {sendEvent} from './ga4-client.js'\nexport type {GA4Event} from './ga4-client.js'\nexport {isConfigured, getClientId, getPlatformGA4Config, GA4_MEASUREMENT_ID, GA4_API_SECRET, GA4_ENDPOINT} from './config.js'\n"],"mappings":";;;;;;;;;;;AAAA,SAAQ,WAAU;","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@hyperdrive.bot/plugin-telemetry",
3
+ "version": "0.1.2",
4
+ "description": "Shared telemetry plugin for DevSquad CLIs — tracks command usage via GA4 Measurement Protocol",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "bin": {
9
+ "plugin-telemetry": "./bin/run.js"
10
+ },
11
+ "files": [
12
+ "/bin",
13
+ "/dist",
14
+ "/oclif.manifest.json"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "build:types": "tsc -b",
19
+ "test": "mocha",
20
+ "prepack": "npm run build && npx oclif manifest || true"
21
+ },
22
+ "oclif": {
23
+ "bin": "plugin-telemetry",
24
+ "commands": "./dist/commands",
25
+ "hooks": {
26
+ "postrun": "./dist/hooks/postrun"
27
+ },
28
+ "topicSeparator": " "
29
+ },
30
+ "peerDependencies": {
31
+ "@oclif/core": "^4"
32
+ },
33
+ "devDependencies": {
34
+ "@oclif/core": "^4",
35
+ "@types/chai": "^4",
36
+ "@types/mocha": "^10",
37
+ "@types/node": "^20",
38
+ "@types/sinon": "^17",
39
+ "chai": "^4",
40
+ "esmock": "^2.7.3",
41
+ "mocha": "^10",
42
+ "oclif": "^4",
43
+ "sinon": "^17",
44
+ "ts-node": "^10",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^5"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "license": "UNLICENSED",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ }
56
+ }