@helpai/elements 0.21.1 → 0.22.0

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, L as Link, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial } from './deployment-B8UxV4b5.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, L as Link, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial } from './deployment-C-Sv0e6E.js';
2
2
  import 'zod';
3
3
 
4
4
  /**
@@ -978,6 +978,45 @@ interface Endpoints {
978
978
  submitForm?: string;
979
979
  }
980
980
 
981
+ /**
982
+ * Built-in anonymous usage tracking — utm.gif-style pixel hits that give
983
+ * the widget OWNER (the site embedding it) insights about how visitors
984
+ * use the widget: events, pages, devices, locales, funnels.
985
+ *
986
+ * Privacy contract (enforced in `src/core/tracking.ts`):
987
+ * - never sends message text, form values, or any user content —
988
+ * event payloads pass through a static per-event allowlist;
989
+ * - the only identifier is the widget's existing anonymous `visitorId`;
990
+ * - geo + browser are derived server-side (IP + User-Agent header),
991
+ * never collected client-side;
992
+ * - `navigator.globalPrivacyControl` / Do-Not-Track force it off
993
+ * regardless of config.
994
+ */
995
+ /** Section: anonymous owner-facing usage tracking. */
996
+ interface TrackingOptions {
997
+ /**
998
+ * Master switch. Default `false` — tracking stays off until the
999
+ * deployment (or the embedding page) explicitly turns it on.
1000
+ */
1001
+ enabled?: boolean;
1002
+ /**
1003
+ * Pixel collector URL. Default: `https://t.<brand domain>/api/ai-elements/px.gif`
1004
+ * (e.g. `https://t.help.ai/api/ai-elements/px.gif`).
1005
+ */
1006
+ endpoint?: string;
1007
+ /**
1008
+ * Per-visitor sampling 0..1. `0.25` tracks ~a quarter of visitors
1009
+ * (decided once per page load, so funnels stay intact). Default `1`.
1010
+ */
1011
+ sampleRate?: number;
1012
+ }
1013
+ /** Post-resolve shape — every field concrete. */
1014
+ interface ResolvedTracking {
1015
+ enabled: boolean;
1016
+ endpoint: string;
1017
+ sampleRate: number;
1018
+ }
1019
+
981
1020
  /**
982
1021
  * The three composing roots: raw input ({@link ChatWidgetOptions}),
983
1022
  * post-normalisation runtime shape ({@link ResolvedOptions}), and the
@@ -1049,6 +1088,8 @@ interface ChatWidgetOptions {
1049
1088
  forms?: FormsOptions;
1050
1089
  /** Messenger modules (Chat / Home / Messages / Help / News tabs). Server-pushable. */
1051
1090
  modules?: ModulesOptions;
1091
+ /** Anonymous owner-facing usage tracking (pixel hits). Server-pushable. */
1092
+ tracking?: TrackingOptions;
1052
1093
  /**
1053
1094
  * Custom client-side storage adapter. Implements the {@link ClientStorage}
1054
1095
  * interface (`get` / `set` / `remove` / `clear`).
@@ -1148,6 +1189,8 @@ interface ResolvedOptions {
1148
1189
  };
1149
1190
  /** Resolved messenger modules — per-module config + ordered enabled list + default tab. */
1150
1191
  modules: ResolvedModules;
1192
+ /** Resolved anonymous usage tracking. */
1193
+ tracking: ResolvedTracking;
1151
1194
  /** Resolved storage adapter — defaults to `LocalStorageAdapter`. */
1152
1195
  storage: ClientStorage;
1153
1196
  onMessage?: ChatWidgetOptions["onMessage"];
@@ -1175,6 +1218,7 @@ interface ServerConfig {
1175
1218
  features?: FeatureFlags;
1176
1219
  forms?: FormsOptions;
1177
1220
  modules?: ModulesOptions;
1221
+ tracking?: TrackingOptions;
1178
1222
  endpoints?: Endpoints;
1179
1223
  }
1180
1224
 
package/index.mjs CHANGED
@@ -468,6 +468,11 @@ var DEFAULT_FEATURES = {
468
468
  humanInLoop: true
469
469
  };
470
470
  var DEFAULT_FORMS = { list: [], byTrigger: {} };
471
+ var DEFAULT_TRACKING = {
472
+ enabled: false,
473
+ endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
474
+ sampleRate: 1
475
+ };
471
476
  var DEFAULT_ENDPOINTS = {
472
477
  upload: "/ai/agent/upload-file",
473
478
  transcribe: "/ai/agent/transcribe-audio",
@@ -562,6 +567,7 @@ function resolveOptions(rawOpts) {
562
567
  poweredBy: resolvePoweredBy(footer.poweredBy),
563
568
  composer: resolveComposer(opts.composer),
564
569
  modules: resolveModules(opts.modules),
570
+ tracking: resolveTracking(opts.tracking),
565
571
  storage: opts.storage ?? defaultStorage,
566
572
  onMessage: opts.onMessage,
567
573
  onOpen: opts.onOpen,
@@ -594,6 +600,16 @@ function resolveHaptics(overrides) {
594
600
  events: overrides?.events
595
601
  };
596
602
  }
603
+ function resolveTracking(overrides) {
604
+ const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
605
+ return {
606
+ enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
607
+ endpoint: overrides?.endpoint ?? DEFAULT_TRACKING.endpoint,
608
+ // Clamp instead of reject — a malformed rate must never turn a
609
+ // disabled tracker on or crash resolve (no Zod on the IIFE path).
610
+ sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1))
611
+ };
612
+ }
597
613
  function resolvePoweredBy(overrides) {
598
614
  const brandDefault = {
599
615
  text: `Powered by ${BRAND.displayName}`,
@@ -736,6 +752,7 @@ var SECTION_KEYS = [
736
752
  "i18n",
737
753
  "footer",
738
754
  "features",
755
+ "tracking",
739
756
  "endpoints"
740
757
  ];
741
758
  function mergeServerConfig(user, server) {
@@ -5580,10 +5597,10 @@ function createHomeNav(initialTab, tabs, onSwitch) {
5580
5597
  sig.value = { ...sig.value, activeTab: tab };
5581
5598
  onSwitch?.(tab);
5582
5599
  },
5583
- push(screen) {
5600
+ push(screen2) {
5584
5601
  const s = sig.value;
5585
5602
  const stack = s.stacks[s.activeTab] ?? [];
5586
- sig.value = { ...s, stacks: { ...s.stacks, [s.activeTab]: [...stack, screen] } };
5603
+ sig.value = { ...s, stacks: { ...s.stacks, [s.activeTab]: [...stack, screen2] } };
5587
5604
  },
5588
5605
  pop() {
5589
5606
  const s = sig.value;
@@ -7242,6 +7259,118 @@ var EventBus = class {
7242
7259
  }
7243
7260
  };
7244
7261
 
7262
+ // src/core/tracking.ts
7263
+ var PROTOCOL_VERSION = "1";
7264
+ var MAX_HITS_PER_MINUTE = 60;
7265
+ var MAX_URL_LENGTH = 2e3;
7266
+ var TRACKED = {
7267
+ // Panel lifecycle — the owner's "is the widget used at all" funnel.
7268
+ open: () => void 0,
7269
+ close: () => void 0,
7270
+ expand: (on) => ({ on }),
7271
+ fullscreen: (on) => ({ on }),
7272
+ popOut: () => void 0,
7273
+ // Conversation funnel. `send` is the key conversion; text never rides.
7274
+ send: (p33) => ({ attachments: p33.attachmentCount }),
7275
+ message: (p33) => ({ role: p33.role }),
7276
+ stop: () => void 0,
7277
+ clear: () => void 0,
7278
+ suggestion: () => void 0,
7279
+ toggleHistory: (p33) => ({ view: p33.view }),
7280
+ conversationStarted: () => void 0,
7281
+ // Forms + human-in-the-loop — ids and outcomes, never values.
7282
+ formSubmit: (p33) => ({ formId: p33.formId, skipped: p33.skipped }),
7283
+ toolResult: () => void 0,
7284
+ toolDecision: (p33) => ({ approved: p33.approved }),
7285
+ // Composer / attachments / voice.
7286
+ attach: (p33) => ({ count: p33.count, bytes: p33.totalBytes }),
7287
+ voiceStart: () => void 0,
7288
+ voiceStop: (p33) => ({ ms: p33.durationMs }),
7289
+ voiceCancel: () => void 0,
7290
+ // Preferences — how visitors tune the surface.
7291
+ localeChange: (locale) => ({ locale }),
7292
+ themeChange: (theme) => ({ theme }),
7293
+ textSizeChange: (size) => ({ size }),
7294
+ soundToggle: (p33) => ({ muted: p33.muted }),
7295
+ sidebarToggle: (p33) => ({ collapsed: p33.collapsed }),
7296
+ calloutDismiss: () => void 0,
7297
+ // Health signal only — the error object itself never leaves the page.
7298
+ error: () => void 0
7299
+ };
7300
+ function privacySignalsOff() {
7301
+ const nav = navigator;
7302
+ return nav.globalPrivacyControl === true || nav.doNotTrack === "1";
7303
+ }
7304
+ function pageUrl() {
7305
+ return location.origin + location.pathname;
7306
+ }
7307
+ function referrerOrigin() {
7308
+ try {
7309
+ return document.referrer ? new URL(document.referrer).origin : "";
7310
+ } catch {
7311
+ return "";
7312
+ }
7313
+ }
7314
+ function createTracker(bus, deps) {
7315
+ const sampleRoll = Math.random();
7316
+ let windowStart = 0;
7317
+ let windowHits = 0;
7318
+ const inflight = /* @__PURE__ */ new Set();
7319
+ const hit = (event, dims) => {
7320
+ const { tracking, publicKey, aiAgentDeploymentId, mode, themeMode } = deps.getOptions();
7321
+ if (!tracking.enabled || privacySignalsOff()) return;
7322
+ if (sampleRoll >= tracking.sampleRate) return;
7323
+ const now = performance.now();
7324
+ if (now - windowStart > 6e4) {
7325
+ windowStart = now;
7326
+ windowHits = 0;
7327
+ }
7328
+ if (++windowHits > MAX_HITS_PER_MINUTE) return;
7329
+ const params = new URLSearchParams();
7330
+ params.set("v", PROTOCOL_VERSION);
7331
+ params.set("e", event);
7332
+ if (publicKey) params.set("pk", publicKey);
7333
+ if (aiAgentDeploymentId) params.set("dep", aiAgentDeploymentId);
7334
+ params.set("vid", deps.getVisitorId());
7335
+ const cid = deps.getConversationId();
7336
+ if (cid) params.set("cid", cid);
7337
+ params.set("url", pageUrl());
7338
+ const ref = referrerOrigin();
7339
+ if (ref) params.set("ref", ref);
7340
+ params.set("sw", String(screen.width));
7341
+ params.set("sh", String(screen.height));
7342
+ params.set("vw", String(innerWidth));
7343
+ params.set("vh", String(innerHeight));
7344
+ params.set("dpr", String(devicePixelRatio));
7345
+ params.set("lang", navigator.language);
7346
+ try {
7347
+ params.set("tz", Intl.DateTimeFormat().resolvedOptions().timeZone);
7348
+ } catch {
7349
+ }
7350
+ params.set("m", mode);
7351
+ params.set("th", themeMode);
7352
+ if (dims) {
7353
+ for (const [k, val] of Object.entries(dims)) {
7354
+ if (val !== void 0) params.set(k, String(val));
7355
+ }
7356
+ }
7357
+ params.set("ts", String(Date.now()));
7358
+ const url = `${tracking.endpoint}?${params}`;
7359
+ if (url.length > MAX_URL_LENGTH) return;
7360
+ const img = new Image(1, 1);
7361
+ inflight.add(img);
7362
+ img.onload = img.onerror = () => inflight.delete(img);
7363
+ img.src = url;
7364
+ };
7365
+ const offs = Object.keys(TRACKED).map(
7366
+ (event) => bus.on(event, (payload) => hit(event, TRACKED[event]?.(payload)))
7367
+ );
7368
+ return () => {
7369
+ for (const off of offs) off();
7370
+ inflight.clear();
7371
+ };
7372
+ }
7373
+
7245
7374
  // src/core/hooks.ts
7246
7375
  var log19 = logger.scope("hooks");
7247
7376
  var HOOK_WILDCARD = "*";
@@ -7381,6 +7510,11 @@ function mount(opts) {
7381
7510
  applyResolvedHostAttributes();
7382
7511
  renderApp();
7383
7512
  });
7513
+ const unsubscribeTracker = createTracker(bus, {
7514
+ getOptions: () => currentOptions,
7515
+ getVisitorId: () => persistence.getVisitorId(),
7516
+ getConversationId: () => persistence.loadConversationId()
7517
+ });
7384
7518
  const host = mountResult.hostElement;
7385
7519
  let unsubscribeHooks = null;
7386
7520
  const applyUpdate = (patch) => {
@@ -7430,6 +7564,7 @@ function mount(opts) {
7430
7564
  destroy: () => {
7431
7565
  log20.info("destroy", { widgetId });
7432
7566
  unsubscribeHooks?.();
7567
+ unsubscribeTracker();
7433
7568
  bus.clear();
7434
7569
  render(null, mountResult.appRoot);
7435
7570
  mountResult.destroy();
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  ],
81
81
  "type": "module",
82
82
  "types": "./index.d.ts",
83
- "version": "0.21.1"
83
+ "version": "0.22.0"
84
84
  }
package/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, L as Link, P as PAGE_AREA_SUGGESTIONS, g as PageContext, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, U as UserContext, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial, h as assetSchema, i as blocksConfigSchema, j as connectionConfigPartialSchema, k as connectionConfigSchema, l as cssColorSchema, m as cssLengthSchema, n as endpointsSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, t as startConversationResponseSchema, u as userContextSchema, v as uuid7Schema, w as widgetConfigPartialSchema, x as widgetConfigSchema, y as widgetSettingsPartialSchema, z as widgetSettingsSchema } from './deployment-B8UxV4b5.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, L as Link, P as PAGE_AREA_SUGGESTIONS, g as PageContext, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, U as UserContext, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial, h as assetSchema, i as blocksConfigSchema, j as connectionConfigPartialSchema, k as connectionConfigSchema, l as cssColorSchema, m as cssLengthSchema, n as endpointsSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, t as startConversationResponseSchema, u as userContextSchema, v as uuid7Schema, w as widgetConfigPartialSchema, x as widgetConfigSchema, y as widgetSettingsPartialSchema, z as widgetSettingsSchema } from './deployment-C-Sv0e6E.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -840,6 +840,22 @@ declare const modulesSchema: z.ZodArray<z.ZodObject<{
840
840
  }, z.core.$loose>>;
841
841
  type ModulesOptions = z.infer<typeof modulesSchema>;
842
842
 
843
+ /**
844
+ * Tracking section schema — built-in anonymous usage analytics for the
845
+ * widget owner (utm.gif-style pixel hits). Off by default; a deployment
846
+ * turns it on to get event / page / device / locale insights without
847
+ * wiring its own analytics. No message content or form values ever ride
848
+ * a hit — payload fields pass a static per-event allowlist in the
849
+ * runtime tracker.
850
+ */
851
+
852
+ declare const trackingSchema: z.ZodObject<{
853
+ enabled: z.ZodOptional<z.ZodBoolean>;
854
+ endpoint: z.ZodOptional<z.ZodURL>;
855
+ sampleRate: z.ZodOptional<z.ZodNumber>;
856
+ }, z.core.$loose>;
857
+ type TrackingSettings = z.infer<typeof trackingSchema>;
858
+
843
859
  /**
844
860
  * Per-mode widget-settings projection.
845
861
  *
@@ -881,4 +897,4 @@ declare const ALL_MODES: readonly Mode[];
881
897
  */
882
898
  declare function emitJsonSchema(): unknown;
883
899
 
884
- export { ALL_MODES, type ActionName, type AttachmentLimits, type Behavior, type CalloutPosition, type CalloutShape, type ComposerOptions, type FeatureFlags, type FeedbackEvent, type FeedbackOptions, type FieldOption, type FieldType, type FieldValidation, type FooterOptions, type FormCondition, type FormDef, type FormField, type Forms, type HapticsOptions, type HeaderActions, type HeaderOptions, type I18nOptions, type LauncherCallout, type LauncherOptions, type LauncherSize, type LauncherVariant, type Mode, type ModuleLayout, type ModuleOptions, type ModulesOptions, type Position, type PoweredBy, type Presentation, type ResizeOptions, type ResponseMode, type SendButtonOptions, type SizeOptions, type SoundOptions, type StringsOverride, type ThemeOverrides, type ThemePreference, type VoiceMode, actionNameSchema, attachmentLimitsSchema, behaviorSchema, calloutPositionSchema, calloutShapeSchema, composerOptionsSchema, emitJsonSchema, featureFlagsSchema, feedbackEventSchema, feedbackSchema, fieldOptionSchema, fieldTypeSchema, fieldValidationSchema, footerSchema, formConditionSchema, formDefSchema, formFieldSchema, formTriggerSchema, formsSchema, hapticsOptionsSchema, headerActionsSchema, headerSchema, i18nSchema, initialSizeSchema, launcherCalloutSchema, launcherOptionsSchema, launcherSizeSchema, launcherVariantSchema, modeSchema, moduleLayoutSchema, moduleSchema, modulesSchema, positionSchema, poweredBySchema, presentationSchema, resizeOptionsSchema, responseModeSchema, sendButtonIconSchema, sendButtonOptionsSchema, sendButtonShapeSchema, sendButtonVariantSchema, sizeOptionsSchema, soundOptionsSchema, stringsOverrideSchema, themeFieldSchema, themeOverridesSchema, themePreferenceSchema, toolRefSchema, voiceModeSchema, widgetSettingsSchemaForMode };
900
+ export { ALL_MODES, type ActionName, type AttachmentLimits, type Behavior, type CalloutPosition, type CalloutShape, type ComposerOptions, type FeatureFlags, type FeedbackEvent, type FeedbackOptions, type FieldOption, type FieldType, type FieldValidation, type FooterOptions, type FormCondition, type FormDef, type FormField, type Forms, type HapticsOptions, type HeaderActions, type HeaderOptions, type I18nOptions, type LauncherCallout, type LauncherOptions, type LauncherSize, type LauncherVariant, type Mode, type ModuleLayout, type ModuleOptions, type ModulesOptions, type Position, type PoweredBy, type Presentation, type ResizeOptions, type ResponseMode, type SendButtonOptions, type SizeOptions, type SoundOptions, type StringsOverride, type ThemeOverrides, type ThemePreference, type TrackingSettings, type VoiceMode, actionNameSchema, attachmentLimitsSchema, behaviorSchema, calloutPositionSchema, calloutShapeSchema, composerOptionsSchema, emitJsonSchema, featureFlagsSchema, feedbackEventSchema, feedbackSchema, fieldOptionSchema, fieldTypeSchema, fieldValidationSchema, footerSchema, formConditionSchema, formDefSchema, formFieldSchema, formTriggerSchema, formsSchema, hapticsOptionsSchema, headerActionsSchema, headerSchema, i18nSchema, initialSizeSchema, launcherCalloutSchema, launcherOptionsSchema, launcherSizeSchema, launcherVariantSchema, modeSchema, moduleLayoutSchema, moduleSchema, modulesSchema, positionSchema, poweredBySchema, presentationSchema, resizeOptionsSchema, responseModeSchema, sendButtonIconSchema, sendButtonOptionsSchema, sendButtonShapeSchema, sendButtonVariantSchema, sizeOptionsSchema, soundOptionsSchema, stringsOverrideSchema, themeFieldSchema, themeOverridesSchema, themePreferenceSchema, toolRefSchema, trackingSchema, voiceModeSchema, widgetSettingsSchemaForMode };
package/schema.json CHANGED
@@ -1839,6 +1839,38 @@
1839
1839
  ]
1840
1840
  ]
1841
1841
  },
1842
+ "tracking": {
1843
+ "type": "object",
1844
+ "properties": {
1845
+ "enabled": {
1846
+ "description": "Master switch. Default `false` — no hits are sent until the deployment turns tracking on.",
1847
+ "type": "boolean"
1848
+ },
1849
+ "endpoint": {
1850
+ "description": "Pixel collector URL. Default: `https://t.<brand domain>/api/ai-elements/px.gif`. Must be a full URL.",
1851
+ "type": "string",
1852
+ "maxLength": 2048,
1853
+ "format": "uri"
1854
+ },
1855
+ "sampleRate": {
1856
+ "description": "Per-visitor sampling 0..1 (decided once per page load, keeping funnels intact). Default `1`.",
1857
+ "type": "number",
1858
+ "minimum": 0,
1859
+ "maximum": 1
1860
+ }
1861
+ },
1862
+ "additionalProperties": {},
1863
+ "description": "Anonymous usage tracking for the widget owner — GA-style pixel events (open, send, form submit, …) with page / device / locale dimensions. Never includes message content; visitors are identified only by the widget's anonymous visitorId.",
1864
+ "examples": [
1865
+ {
1866
+ "enabled": true
1867
+ },
1868
+ {
1869
+ "enabled": true,
1870
+ "sampleRate": 0.5
1871
+ }
1872
+ ]
1873
+ },
1842
1874
  "site": {
1843
1875
  "description": "Site config — applies in `mode: \"page\"`. Comes from the start-conversation response, not user-edited."
1844
1876
  },
@@ -3556,6 +3588,38 @@
3556
3588
  }
3557
3589
  ]
3558
3590
  ]
3591
+ },
3592
+ "tracking": {
3593
+ "type": "object",
3594
+ "properties": {
3595
+ "enabled": {
3596
+ "description": "Master switch. Default `false` — no hits are sent until the deployment turns tracking on.",
3597
+ "type": "boolean"
3598
+ },
3599
+ "endpoint": {
3600
+ "description": "Pixel collector URL. Default: `https://t.<brand domain>/api/ai-elements/px.gif`. Must be a full URL.",
3601
+ "type": "string",
3602
+ "maxLength": 2048,
3603
+ "format": "uri"
3604
+ },
3605
+ "sampleRate": {
3606
+ "description": "Per-visitor sampling 0..1 (decided once per page load, keeping funnels intact). Default `1`.",
3607
+ "type": "number",
3608
+ "minimum": 0,
3609
+ "maximum": 1
3610
+ }
3611
+ },
3612
+ "additionalProperties": {},
3613
+ "description": "Anonymous usage tracking for the widget owner — GA-style pixel events (open, send, form submit, …) with page / device / locale dimensions. Never includes message content; visitors are identified only by the widget's anonymous visitorId.",
3614
+ "examples": [
3615
+ {
3616
+ "enabled": true
3617
+ },
3618
+ {
3619
+ "enabled": true,
3620
+ "sampleRate": 0.5
3621
+ }
3622
+ ]
3559
3623
  }
3560
3624
  },
3561
3625
  "additionalProperties": {}