relay-dsh-plugin-manager 0.2.4 → 0.2.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [0.2.5] - 2026-09-07
6
+
7
+ ### Added
8
+
9
+ - Default-on anonymous operational analytics for plugin-manager use and
10
+ manager-executed install start, success, and failure outcomes, with explicit
11
+ opt-out through `RELAY_PLUGIN_MANAGER_TELEMETRY=0`.
12
+ - First-party delivery to the Registry's versioned telemetry endpoint; the npm
13
+ package contains no PostHog host or project key.
14
+ - A locally persisted random anonymous identifier; search text, installed
15
+ inventory, local paths, command output, error messages, IP geolocation, and
16
+ account identity are excluded.
17
+
5
18
  ## [0.2.4] - 2026-09-05
6
19
 
7
20
  ### Changed
package/README.md CHANGED
@@ -100,6 +100,32 @@ missing companion peers, waits for one confirmation, and runs the installs in
100
100
  sequence. Completed changes that still need restart are reported explicitly as
101
101
  restart-required or waiting for a manual restart.
102
102
 
103
+ ### Anonymous usage analytics
104
+
105
+ Anonymous operational analytics are enabled by default to measure plugin-manager
106
+ search and installation outcomes. To opt out without disabling plugin management,
107
+ set this when starting DSH Web:
108
+
109
+ ```bash
110
+ RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
111
+ ```
112
+
113
+ The manager sends events only to the first-party
114
+ `https://dsh-plugins.tech/v1/telemetry/events` endpoint. The Registry validates
115
+ the exact event schema and forwards accepted events to its PostHog analytics
116
+ project. The plugin records only the manager action type and plugin
117
+ install start, success, or failure events produced by the manager. Install
118
+ events contain the public package name, activation/restart booleans, and a
119
+ bounded error code. Search terms, conversation content, local paths, command
120
+ output, error text, IP geolocation, and account identity are not sent. A random
121
+ anonymous ID is stored in `.relay-plugin-manager/telemetry.json` inside the
122
+ current Profile to avoid counting one installation as many users; delete the
123
+ file to reset it.
124
+
125
+ These events measure manager use and manager-executed installs. They do not
126
+ claim that another plugin's feature was actually used merely because it loaded
127
+ or activated.
128
+
103
129
  ### DSH Registry discovery provider
104
130
 
105
131
  The source-only Registry search provider uses `https://dsh-plugins.tech` by
package/README.zh.md CHANGED
@@ -95,6 +95,26 @@ dsh web
95
95
  一次确认后依次安装。修改完成但仍需重启时,状态会明确区分“需要另行重启”和
96
96
  “等待人工重启”。
97
97
 
98
+ ### 匿名使用统计
99
+
100
+ 匿名运行统计默认开启,用于衡量插件管理器的搜索和安装结果。如需关闭,可在启动
101
+ DSH Web 时设置以下变量;关闭统计不会影响插件管理功能:
102
+
103
+ ```bash
104
+ RELAY_PLUGIN_MANAGER_TELEMETRY=0 dsh web
105
+ ```
106
+
107
+ 管理器只向第一方接口 `https://dsh-plugins.tech/v1/telemetry/events` 发送事件;Registry
108
+ 会校验严格的事件结构,再把通过校验的事件转发到自己的 PostHog 项目。统计只记录
109
+ 管理器操作类型,以及由管理器执行的插件安装开始、成功或失败;安装
110
+ 事件包含公开的插件包名、是否激活、是否需要重启和有限错误代码。不会发送搜索词、
111
+ 对话内容、本地路径、命令输出、错误文本、IP 地理位置或登录身份。一个随机匿名 ID
112
+ 保存在当前 Profile 的 `.relay-plugin-manager/telemetry.json`,用于避免把同一安装重复
113
+ 计算为多个用户;删除该文件即可重置。
114
+
115
+ 这些数据只能说明“插件管理器被使用”和“管理器执行了安装”,不会把插件加载或
116
+ 激活推断成其他插件的真实功能使用。
117
+
98
118
  ### DSH Registry 发现数据源
99
119
 
100
120
  来源级 Registry 搜索 provider 默认使用 `https://dsh-plugins.tech`。可设置插件
package/SPEC.md CHANGED
@@ -51,6 +51,7 @@ service.
51
51
  | PM-024 | GitHub owner discovery recognizes `owner:<name>`, owner-only GitHub identities, `<name> DSH plugins`, and conservative bare identifiers containing digits. It sends a typed owner intent to providers, uses GitHub's exact owner qualifier, case-insensitively verifies returned ownership, and ranks verified owner matches first. Owner-only `inspect` fails with an actionable error directing callers to search. Every emitted repository identity is accepted by `inspect`; every recommended immutable source is accepted by `plan`. |
52
52
  | PM-025 | Register a read-only DSH Registry provider against `https://dsh-plugins.tech` by default, with an explicit `registryUrl: false` opt-out and HTTPS-only configuration override. Send only the task query, inferred Chinese/English locale and result limit; accept only source-level untrusted DiscoveryEntry records, convert their npm/GitHub descriptors into core-owned source types, and submit them to the same mandatory local inspection used by all providers. Registry responses cannot supply exact versions, approval, plans, installers, or profile state, and provider failure remains isolated. |
53
53
  | PM-026 | Return search candidates as one explicit, one-based relevance-ranked result page with a default and maximum size of 20. The model-facing contract requires the Agent to preserve rank order, surface every possibly relevant returned candidate, exclude candidates whose purpose is clearly unrelated, and MUST NOT silently reduce the remaining page to a fixed top-N. Ranking remains distinct from compatibility, security, or installation approval. |
54
+ | PM-027 | Send default-on anonymous operational telemetry only to the Registry's canonical first-party HTTPS endpoint under schema `1.0.0`, with explicit `RELAY_PLUGIN_MANAGER_TELEMETRY=0` or `telemetry.enabled: false` opt-out. Events are limited to manager discovery/planning type and manager-executed install start/success/failure, including per-plugin batch outcomes. Never send raw search text, conversation content, Profile state, installed inventory, paths, credentials, command output, error text, account identity, PostHog configuration, or client timestamps. Store only a stable random anonymous ID locally, create it lazily, and never let storage, validation, network, Registry, or analytics failure affect plugin management. |
54
55
 
55
56
  ## Command Grammar
56
57
 
@@ -39,6 +39,7 @@ This matrix is the release contract. Tests name the relevant `PM-*` ids.
39
39
  | A-031 search results feed one immutable multi-install plan through the real DSH tool and user-question registries; no mutation starts before one exact UI approval, then child installs execute serially and approval replay is rejected | PM-002, PM-005, PM-009, PM-010, PM-015, PM-023, PM-024 | combined host/tool/search/question/manager integration |
40
40
  | A-032 The default formal Registry searches at least 3,000 source records for Chinese task text, English task text, exact plugin names and an honest empty result; every returned source is locally inspected to an immutable npm version or GitHub commit, while Registry authority remains source-only, untrusted, non-recommending and non-approving | PM-005, PM-006, PM-025 | provider/unit/host integration tests plus `npm run acceptance:live:registry` |
41
41
  | A-033 Search defaults to a complete twenty-candidate result page, assigns stable one-based display ranks after merge and deduplication, and tells the Agent to exclude clearly unrelated purposes while surfacing every remaining possibly relevant candidate without silent fixed-count truncation | PM-005, PM-026 | search orchestration and conversation-tool contract unit tests plus Agent answer acceptance |
42
+ | A-034 Default manager actions send only schema-valid anonymous envelopes to the first-party Registry endpoint, reuse a lazily created random ID, record each attempted batch install outcome, and continue normally through storage or delivery failure; explicit opt-out creates no ID and makes no telemetry request | PM-027 | telemetry unit tests + manager integration event assertions + Registry endpoint contract and end-to-end PostHog verification |
42
43
 
43
44
  ## Test Layers
44
45
 
package/lib/index.d.ts CHANGED
@@ -2,6 +2,18 @@ import { a as PluginSearchRuntime, c as PluginSource, i as PluginSearchRequest,
2
2
  import { spawn } from "node:child_process";
3
3
  import { Context } from "@deepseek-ai/cordis";
4
4
 
5
+ //#region src/telemetry.d.ts
6
+ type TelemetryProperty = string | number | boolean;
7
+ interface Telemetry {
8
+ capture(event: string, properties?: Readonly<Record<string, TelemetryProperty>>): void;
9
+ }
10
+ interface TelemetryConfig {
11
+ /** Anonymous operational telemetry is enabled unless this is explicitly false. */
12
+ enabled?: boolean;
13
+ /** Registry telemetry endpoint. Only the canonical service or localhost is accepted. */
14
+ endpoint?: string;
15
+ }
16
+ //#endregion
5
17
  //#region src/profile.d.ts
6
18
  interface PackageSurface {
7
19
  packageName: string;
@@ -238,6 +250,7 @@ interface PluginManagerDependencies {
238
250
  operations?: OperationTracker;
239
251
  fetchOptions?: Omit<FetchOptions, 'signal'>;
240
252
  hmrTimeoutMs?: number;
253
+ telemetry?: Telemetry;
241
254
  }
242
255
  interface DiscoverRequest {
243
256
  action: 'list' | 'search' | 'inspect' | 'status';
@@ -301,7 +314,9 @@ declare class PluginManager {
301
314
  private readonly operations;
302
315
  private readonly fetchOptions;
303
316
  private readonly hmrTimeoutMs;
317
+ private readonly telemetry;
304
318
  constructor(dependencies: PluginManagerDependencies);
319
+ private capture;
305
320
  private loaderEntries;
306
321
  list(): PluginStatus[];
307
322
  discover(request: DiscoverRequest, signal?: AbortSignal): Promise<unknown>;
@@ -331,8 +346,9 @@ declare const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
331
346
  interface Config {
332
347
  allowRestart?: boolean;
333
348
  registryUrl?: string | false;
349
+ telemetry?: TelemetryConfig;
334
350
  }
335
351
  declare function apply(ctx: Context, config?: Config): void;
336
352
  //#endregion
337
- export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, apply, inject, name };
353
+ export { Config, DEFAULT_REGISTRY_ORIGIN, type DiscoverRequest, type InstallManyItemResult, type InstallManyItemStatus, type InstallManyResult, type MutationResult, type PlanRequest, type PluginInspection, PluginManager, type PluginSearchCandidate, type PluginSearchMatch, type PluginSearchProvider, type PluginSearchRequest, type PluginSource, type TelemetryConfig, apply, inject, name };
338
354
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -1223,11 +1223,56 @@ const PROTECTED_ENTRY_IDS = /* @__PURE__ */ new Set([
1223
1223
  "webserver",
1224
1224
  "web-runtime"
1225
1225
  ]);
1226
+ const TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
1227
+ "INVALID_SOURCE",
1228
+ "INVALID_NPM_SPEC",
1229
+ "INVALID_NPM_VERSION",
1230
+ "INVALID_GITHUB_SPEC",
1231
+ "INVALID_GITHUB_REF",
1232
+ "GITHUB_OWNER_REQUIRES_SEARCH",
1233
+ "IMMUTABLE_SOURCE_REQUIRED",
1234
+ "NETWORK_ERROR",
1235
+ "SOURCE_HTTP_ERROR",
1236
+ "INVALID_SOURCE_METADATA",
1237
+ "INVALID_PLUGIN_MANIFEST",
1238
+ "NOT_DSH_PLUGIN",
1239
+ "PACKAGE_NAME_MISMATCH",
1240
+ "NPM_INTEGRITY_MISSING",
1241
+ "INVALID_SEARCH_QUERY",
1242
+ "INVALID_ACTION",
1243
+ "INVALID_BATCH",
1244
+ "DUPLICATE_SEARCH_PROVIDER",
1245
+ "PROFILE_READ_FAILED",
1246
+ "PROFILE_WRITE_FAILED",
1247
+ "PLUGIN_NOT_INSTALLED",
1248
+ "PLUGIN_ALREADY_INSTALLED",
1249
+ "ENABLEMENT_UNSUPPORTED",
1250
+ "ENABLEMENT_CONFLICT",
1251
+ "PROTECTED_PLUGIN",
1252
+ "CONFIRMATION_REQUIRED",
1253
+ "CONFIRMATION_INVALID",
1254
+ "CONFIRMATION_EXPIRED",
1255
+ "CONFIRMATION_REPLAYED",
1256
+ "PLAN_STALE",
1257
+ "OPERATION_NOT_FOUND",
1258
+ "DSH_COMMAND_FAILED",
1259
+ "BATCH_INSTALL_FAILED",
1260
+ "POSTCONDITION_FAILED",
1261
+ "RESTART_UNAVAILABLE"
1262
+ ]);
1226
1263
  function safePackageName(value) {
1227
1264
  const name = value?.trim() ?? "";
1228
1265
  if (!NPM_NAME.test(name)) fail("INVALID_NPM_SPEC", "A valid installed package name is required.");
1229
1266
  return name;
1230
1267
  }
1268
+ function queryLengthBucket(value) {
1269
+ const length = value?.trim().length ?? 0;
1270
+ if (length === 0) return "empty";
1271
+ if (length <= 10) return "1-10";
1272
+ if (length <= 30) return "11-30";
1273
+ if (length <= 80) return "31-80";
1274
+ return "81+";
1275
+ }
1231
1276
  function commandResult(result) {
1232
1277
  return {
1233
1278
  exitCode: result.exitCode,
@@ -1241,6 +1286,10 @@ function operationError(error) {
1241
1286
  message: error instanceof Error ? error.message : String(error)
1242
1287
  };
1243
1288
  }
1289
+ function telemetryErrorCode(error) {
1290
+ const code = operationError(error).code;
1291
+ return code !== void 0 && TELEMETRY_ERROR_CODES.has(code) ? code : "UNKNOWN";
1292
+ }
1244
1293
  function installedPackageManifest(profileDir, packageName) {
1245
1294
  try {
1246
1295
  return JSON.parse(readFileSync(join(profileDir, "node_modules", packageName, "package.json"), "utf8"));
@@ -1265,6 +1314,7 @@ var PluginManager = class {
1265
1314
  operations;
1266
1315
  fetchOptions;
1267
1316
  hmrTimeoutMs;
1317
+ telemetry;
1268
1318
  constructor(dependencies) {
1269
1319
  this.profileDir = dependencies.profileDir;
1270
1320
  this.searchRuntime = dependencies.searchRuntime;
@@ -1277,6 +1327,12 @@ var PluginManager = class {
1277
1327
  this.operations = dependencies.operations ?? new OperationTracker();
1278
1328
  this.fetchOptions = dependencies.fetchOptions ?? {};
1279
1329
  this.hmrTimeoutMs = dependencies.hmrTimeoutMs ?? 5e3;
1330
+ this.telemetry = dependencies.telemetry ?? { capture() {} };
1331
+ }
1332
+ capture(event, properties = {}) {
1333
+ try {
1334
+ this.telemetry.capture(event, properties);
1335
+ } catch {}
1280
1336
  }
1281
1337
  loaderEntries() {
1282
1338
  if (this.loader === void 0) return [];
@@ -1297,6 +1353,14 @@ var PluginManager = class {
1297
1353
  return listPluginStatuses(this.profileDir, this.loaderEntries());
1298
1354
  }
1299
1355
  async discover(request, signal) {
1356
+ this.capture("plugin_manager_used", {
1357
+ surface: "discover",
1358
+ action: request.action,
1359
+ ...request.action === "search" ? {
1360
+ has_query: (request.query?.trim().length ?? 0) > 0,
1361
+ query_length_bucket: queryLengthBucket(request.query)
1362
+ } : {}
1363
+ });
1300
1364
  if (request.action === "list") return {
1301
1365
  profile: "web",
1302
1366
  plugins: this.list()
@@ -1360,6 +1424,11 @@ var PluginManager = class {
1360
1424
  });
1361
1425
  }
1362
1426
  async plan(request, signal) {
1427
+ this.capture("plugin_manager_used", {
1428
+ surface: "plan",
1429
+ action: request.operation,
1430
+ ...request.operation === "install_many" ? { batch_size: request.sources?.length ?? 0 } : {}
1431
+ });
1363
1432
  if (request.operation === "install_many") return await this.planInstallMany(request.sources, signal);
1364
1433
  if (request.operation === "restart") {
1365
1434
  if (!this.restarter.available()) fail("RESTART_UNAVAILABLE", "Automatic restart is unavailable in this deployment.");
@@ -1486,7 +1555,22 @@ var PluginManager = class {
1486
1555
  if (plan.packageName === void 0) fail("POSTCONDITION_FAILED", "Mutation plan has no package name.");
1487
1556
  if (plan.action === "install" || plan.action === "update") {
1488
1557
  if (plan.installSpec === void 0) fail("POSTCONDITION_FAILED", "Install/update plan has no immutable source.");
1489
- return this.withRestartGuidance(await this.installOrUpdate(plan.action, plan.packageName, plan.installSpec, context), automaticRestartAvailable);
1558
+ if (plan.action === "install") this.capture("plugin_install_started", { plugin_name: plan.packageName });
1559
+ try {
1560
+ const result = this.withRestartGuidance(await this.installOrUpdate(plan.action, plan.packageName, plan.installSpec, context), automaticRestartAvailable);
1561
+ if (plan.action === "install") this.capture("plugin_install_succeeded", {
1562
+ plugin_name: plan.packageName,
1563
+ activated: result.activated === true,
1564
+ restart_required: result.restartRequired
1565
+ });
1566
+ return result;
1567
+ } catch (error) {
1568
+ if (plan.action === "install") this.capture("plugin_install_failed", {
1569
+ plugin_name: plan.packageName,
1570
+ error_code: telemetryErrorCode(error)
1571
+ });
1572
+ throw error;
1573
+ }
1490
1574
  }
1491
1575
  if (plan.action === "remove") return this.withRestartGuidance(await this.remove(plan.packageName, context), automaticRestartAvailable);
1492
1576
  return this.withRestartGuidance(await this.toggle(plan.action, plan.packageName, context), automaticRestartAvailable);
@@ -1534,6 +1618,10 @@ var PluginManager = class {
1534
1618
  break;
1535
1619
  }
1536
1620
  context.progress(`install_many: ${index + 1}/${items.length} installing ${item.packageName}`);
1621
+ this.capture("plugin_install_started", {
1622
+ plugin_name: item.packageName,
1623
+ batch: true
1624
+ });
1537
1625
  try {
1538
1626
  const result = this.withRestartGuidance(await this.installOrUpdate("install", item.packageName, item.installSpec, context), automaticRestartAvailable);
1539
1627
  results.push({
@@ -1542,6 +1630,12 @@ var PluginManager = class {
1542
1630
  status: this.mutationCompletion(result, automaticRestartAvailable).status,
1543
1631
  result
1544
1632
  });
1633
+ this.capture("plugin_install_succeeded", {
1634
+ plugin_name: item.packageName,
1635
+ batch: true,
1636
+ activated: result.activated === true,
1637
+ restart_required: result.restartRequired
1638
+ });
1545
1639
  } catch (error) {
1546
1640
  results.push({
1547
1641
  packageName: item.packageName,
@@ -1549,6 +1643,11 @@ var PluginManager = class {
1549
1643
  status: context.signal.aborted ? "cancelled" : "failed",
1550
1644
  error: operationError(error)
1551
1645
  });
1646
+ this.capture("plugin_install_failed", {
1647
+ plugin_name: item.packageName,
1648
+ batch: true,
1649
+ error_code: telemetryErrorCode(error)
1650
+ });
1552
1651
  skipRemaining(index + 1);
1553
1652
  break;
1554
1653
  }
@@ -2078,6 +2177,107 @@ var DshCliRunner = class {
2078
2177
  }
2079
2178
  };
2080
2179
  //#endregion
2180
+ //#region src/telemetry.ts
2181
+ const DEFAULT_ENDPOINT = "https://dsh-plugins.tech/v1/telemetry/events";
2182
+ const STATE_DIRECTORY = ".relay-plugin-manager";
2183
+ const STATE_FILE = "telemetry.json";
2184
+ const SCHEMA_VERSION = "1.0.0";
2185
+ const EVENTS = /* @__PURE__ */ new Set([
2186
+ "plugin_manager_used",
2187
+ "plugin_install_started",
2188
+ "plugin_install_succeeded",
2189
+ "plugin_install_failed"
2190
+ ]);
2191
+ const noopTelemetry = Object.freeze({ capture() {} });
2192
+ function safeEndpoint(value) {
2193
+ try {
2194
+ const parsed = new URL(value ?? DEFAULT_ENDPOINT);
2195
+ const local = [
2196
+ "localhost",
2197
+ "127.0.0.1",
2198
+ "::1"
2199
+ ].includes(parsed.hostname);
2200
+ const canonical = parsed.protocol === "https:" && parsed.hostname === "dsh-plugins.tech";
2201
+ if (!local && !canonical || local && !["http:", "https:"].includes(parsed.protocol)) return null;
2202
+ if (parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/v1/telemetry/events" || parsed.search !== "" || parsed.hash !== "") return null;
2203
+ return parsed.href;
2204
+ } catch {
2205
+ return null;
2206
+ }
2207
+ }
2208
+ function anonymousId(profileDir, random) {
2209
+ const directory = join(profileDir, STATE_DIRECTORY);
2210
+ const path = join(directory, STATE_FILE);
2211
+ try {
2212
+ const existing = JSON.parse(readFileSync(path, "utf8"));
2213
+ if (typeof existing.anonymousId === "string" && /^[0-9a-f-]{36}$/iu.test(existing.anonymousId)) return existing.anonymousId;
2214
+ } catch {}
2215
+ const id = random();
2216
+ try {
2217
+ mkdirSync(directory, {
2218
+ recursive: true,
2219
+ mode: 448
2220
+ });
2221
+ writeFileSync(path, `${JSON.stringify({ anonymousId: id })}\n`, { mode: 384 });
2222
+ } catch {}
2223
+ return id;
2224
+ }
2225
+ function allowedProperties(event, properties) {
2226
+ const keys = new Set(Object.keys(properties));
2227
+ const exact = (required, optional = []) => {
2228
+ if (required.some((key) => !keys.has(key))) return false;
2229
+ return [...keys].every((key) => required.includes(key) || optional.includes(key));
2230
+ };
2231
+ if (event === "plugin_manager_used") {
2232
+ if (!exact(["surface", "action"], [
2233
+ "has_query",
2234
+ "query_length_bucket",
2235
+ "batch_size"
2236
+ ])) return false;
2237
+ if (!["discover", "plan"].includes(String(properties.surface))) return false;
2238
+ return typeof properties.action === "string";
2239
+ }
2240
+ if (event === "plugin_install_started") return exact(["plugin_name"], ["batch"]);
2241
+ if (event === "plugin_install_succeeded") return exact([
2242
+ "plugin_name",
2243
+ "activated",
2244
+ "restart_required"
2245
+ ], ["batch"]);
2246
+ if (event === "plugin_install_failed") return exact(["plugin_name", "error_code"], ["batch"]);
2247
+ return false;
2248
+ }
2249
+ function createTelemetry(profileDir, config, runtime = {
2250
+ fetch,
2251
+ random: randomUUID
2252
+ }) {
2253
+ if (config?.enabled === false) return noopTelemetry;
2254
+ const endpoint = safeEndpoint(config?.endpoint);
2255
+ if (endpoint === null) return noopTelemetry;
2256
+ let distinctId;
2257
+ return Object.freeze({ capture(event, properties = {}) {
2258
+ if (!EVENTS.has(event) || !allowedProperties(event, properties)) return;
2259
+ distinctId ??= anonymousId(profileDir, runtime.random);
2260
+ const controller = new AbortController();
2261
+ const timeout = setTimeout(() => controller.abort(), 5e3);
2262
+ timeout.unref?.();
2263
+ try {
2264
+ runtime.fetch(endpoint, {
2265
+ method: "POST",
2266
+ headers: { "content-type": "application/json" },
2267
+ body: JSON.stringify({
2268
+ schema_version: SCHEMA_VERSION,
2269
+ anonymous_id: distinctId,
2270
+ event,
2271
+ properties
2272
+ }),
2273
+ signal: controller.signal
2274
+ }).catch(() => void 0).finally(() => clearTimeout(timeout));
2275
+ } catch {
2276
+ clearTimeout(timeout);
2277
+ }
2278
+ } });
2279
+ }
2280
+ //#endregion
2081
2281
  //#region src/index.ts
2082
2282
  const name = "relay-dsh-plugin-manager";
2083
2283
  const inject = [
@@ -2090,6 +2290,10 @@ const inject = [
2090
2290
  const DEFAULT_REGISTRY_ORIGIN = "https://dsh-plugins.tech";
2091
2291
  function apply(ctx, config = {}) {
2092
2292
  const profileDir = profileDirectory("web");
2293
+ const telemetry = config.telemetry ?? {
2294
+ enabled: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY !== "0",
2295
+ endpoint: process.env.RELAY_PLUGIN_MANAGER_TELEMETRY_ENDPOINT
2296
+ };
2093
2297
  ctx.pluginSearch.register(npmSearchProvider());
2094
2298
  ctx.pluginSearch.register(githubSearchProvider());
2095
2299
  const configuredRegistryUrl = config.registryUrl ?? process.env.DSH_PLUGIN_REGISTRY_URL?.trim();
@@ -2101,7 +2305,8 @@ function apply(ctx, config = {}) {
2101
2305
  runner: new DshCliRunner(),
2102
2306
  hot: new HotRuntime(ctx, profileDir),
2103
2307
  restarter: new DshRestarter({ allowRestart: config.allowRestart }),
2104
- loader: ctx.loader
2308
+ loader: ctx.loader,
2309
+ telemetry: createTelemetry(profileDir, telemetry)
2105
2310
  }));
2106
2311
  }
2107
2312
  //#endregion