@sensiblestats/widget-react 0.43.1 → 0.44.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/dist/index.js CHANGED
@@ -279,6 +279,18 @@ function isTerminal(result) {
279
279
  if (result.status === "price_changed") return result.accepted;
280
280
  return result.status === "added" || result.status === "duplicate";
281
281
  }
282
+ function slipOutcome(result) {
283
+ if (!result) return "added";
284
+ switch (result.status) {
285
+ case "added":
286
+ case "duplicate":
287
+ return result.status;
288
+ case "price_changed":
289
+ return result.accepted ? "added" : "price_changed";
290
+ case "rejected":
291
+ return result.reason;
292
+ }
293
+ }
282
294
  function resultToToast(result, shownOdds, ui) {
283
295
  if (!result) return { kind: "success", text: ui.slipAdded };
284
296
  switch (result.status) {
@@ -300,8 +312,28 @@ import { ENTITY_TYPE_CODES } from "@sensiblestats/widget-sdk";
300
312
  function isSignalEntityType(value) {
301
313
  return Object.prototype.hasOwnProperty.call(ENTITY_TYPE_CODES, value);
302
314
  }
303
- function createDashboardTapSignal(sendSignal) {
315
+ function toWidgetEntityType(value) {
316
+ switch (value.toLowerCase()) {
317
+ case "match":
318
+ return "match";
319
+ case "team":
320
+ return "team";
321
+ case "player":
322
+ return "player";
323
+ case "competition":
324
+ return "competition";
325
+ case "market":
326
+ return "market";
327
+ default:
328
+ return null;
329
+ }
330
+ }
331
+ function createDashboardTapSignal(sendSignal, log) {
304
332
  return (tile, index, layout) => {
333
+ const widgetEntityType = toWidgetEntityType(tile.signal.entityType);
334
+ if (widgetEntityType) {
335
+ log?.log({ name: "tile_tap", entityType: widgetEntityType, entityId: tile.signal.entityId });
336
+ }
305
337
  if (!isSignalEntityType(tile.signal.entityType)) return;
306
338
  try {
307
339
  void sendSignal({
@@ -1373,6 +1405,13 @@ function useLiveMonitor(opts) {
1373
1405
  };
1374
1406
  }
1375
1407
 
1408
+ // src/eventLogContext.ts
1409
+ import { createContext, useContext } from "react";
1410
+ var EventLogContext = createContext(null);
1411
+ function useEventLog() {
1412
+ return useContext(EventLogContext);
1413
+ }
1414
+
1376
1415
  // src/components/ChatFab.tsx
1377
1416
  import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
1378
1417
 
@@ -1701,19 +1740,32 @@ function withTimeout(value) {
1701
1740
  new Promise((_, reject) => setTimeout(() => reject(new Error("add-to-slip timed out")), ADD_TO_SLIP_TIMEOUT_MS))
1702
1741
  ]);
1703
1742
  }
1704
- function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1743
+ function SlipButton({ selection, shownOdds, marketName, marketDeveloperName, slip, ui }) {
1705
1744
  const [pending, setPending] = useState6(false);
1706
1745
  const [settled, setSettled] = useState6(false);
1707
1746
  const [toast, setToast] = useState6(null);
1708
1747
  const dismiss = useCallback3(() => setToast(null), []);
1748
+ const log = useEventLog();
1709
1749
  const handler = slip.onAddToSlip;
1710
1750
  if (!ui || !handler || !selection) return null;
1711
1751
  const onClick = async () => {
1712
1752
  setPending(true);
1753
+ if (marketDeveloperName !== void 0) {
1754
+ log?.log({ name: "slip_click", entityType: "market", marketDeveloperName, entityId: marketDeveloperName });
1755
+ }
1713
1756
  try {
1714
1757
  const raw = await withTimeout(Promise.resolve(handler(selection)));
1715
1758
  if (raw != null && !isAddToSlipResult(raw)) throw new Error("malformed add-to-slip result");
1716
1759
  const result = raw ?? void 0;
1760
+ if (marketDeveloperName !== void 0) {
1761
+ log?.log({
1762
+ name: "slip_result",
1763
+ entityType: "market",
1764
+ marketDeveloperName,
1765
+ entityId: marketDeveloperName,
1766
+ outcome: slipOutcome(result)
1767
+ });
1768
+ }
1717
1769
  setToast(resultToToast(result, shownOdds || selection.oddsDecimal.toFixed(2), ui));
1718
1770
  setSettled(isTerminal(result));
1719
1771
  if (wasAdded(result) && slip.onMarketClicked) {
@@ -1723,6 +1775,9 @@ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1723
1775
  }
1724
1776
  }
1725
1777
  } catch {
1778
+ if (marketDeveloperName !== void 0) {
1779
+ log?.log({ name: "slip_result", entityType: "market", marketDeveloperName, entityId: marketDeveloperName, outcome: "timeout" });
1780
+ }
1726
1781
  setToast({ kind: "error", text: ui.slipFailed });
1727
1782
  } finally {
1728
1783
  setPending(false);
@@ -1744,7 +1799,17 @@ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
1744
1799
  }
1745
1800
  function AddToSlipButton({ it, slip, ui }) {
1746
1801
  const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
1747
- return /* @__PURE__ */ jsx9(SlipButton, { selection, shownOdds: it.odds ?? "", marketName: it.market, slip, ui });
1802
+ return /* @__PURE__ */ jsx9(
1803
+ SlipButton,
1804
+ {
1805
+ selection,
1806
+ shownOdds: it.odds ?? "",
1807
+ marketName: it.market,
1808
+ marketDeveloperName: it.marketDeveloperName,
1809
+ slip,
1810
+ ui
1811
+ }
1812
+ );
1748
1813
  }
1749
1814
 
1750
1815
  // src/components/OddsMovementBadge.tsx
@@ -1794,13 +1859,21 @@ var PIN_GLYPHS = {
1794
1859
  function PinToggle({ pin, monitor, ui }) {
1795
1860
  const [toast, setToast] = useState8(null);
1796
1861
  const dismiss = useCallback4(() => setToast(null), []);
1862
+ const log = useEventLog();
1797
1863
  if (!monitor || !monitor.enabled) return null;
1798
1864
  const key = pinKey(pin);
1799
1865
  const pinned = monitor.isPinned(key);
1800
1866
  const labels = pin.kind === "match" ? { pin: ui.matchPinLabel, unpin: ui.matchUnpinLabel } : { pin: ui.pinLabel, unpin: ui.unpinLabel };
1801
1867
  const onClick = () => {
1802
1868
  const result = monitor.togglePin({ ...pin, key });
1803
- if (result === "match_limit") {
1869
+ if ((result === "added" || result === "removed") && (pin.kind === "match" || pin.marketDeveloperName !== void 0)) {
1870
+ log?.log({
1871
+ name: result === "added" ? "pin_add" : "pin_remove",
1872
+ entityType: pin.kind === "match" ? "match" : "market",
1873
+ entityId: pin.kind === "match" ? String(pin.matchId) : pin.marketDeveloperName,
1874
+ marketDeveloperName: pin.marketDeveloperName
1875
+ });
1876
+ } else if (result === "match_limit") {
1804
1877
  setToast({ kind: "error", text: ui.matchLimitToast });
1805
1878
  } else if (result === "market_limit") {
1806
1879
  setToast({ kind: "error", text: ui.marketLimitToast });
@@ -1857,11 +1930,19 @@ function hasStatsContent(it) {
1857
1930
  }
1858
1931
  function ReasonStrip({ it, ui }) {
1859
1932
  const [open, setOpen] = useState9(false);
1933
+ const log = useEventLog();
1860
1934
  const hasScopes = !!it.scopes && it.scopes.length >= 2;
1861
1935
  const hasStats = hasStatsContent(it);
1862
1936
  const hasText = !!it.text;
1863
1937
  if (!hasText && !hasStats) return null;
1864
1938
  const expandable = hasStats || (it.text?.length ?? 0) > 90;
1939
+ const onToggle = () => {
1940
+ setOpen((v) => {
1941
+ const next = !v;
1942
+ if (next) log?.log({ name: "insight_expand", entityType: "match", entityId: String(it.matchId) });
1943
+ return next;
1944
+ });
1945
+ };
1865
1946
  return /* @__PURE__ */ jsxs9("div", { className: "ss-w-bi-reason", children: [
1866
1947
  /* @__PURE__ */ jsx13("span", { className: "ss-w-bi-chart", "aria-hidden": "true" }),
1867
1948
  /* @__PURE__ */ jsxs9("div", { className: "ss-w-bi-reason-body", children: [
@@ -1875,7 +1956,7 @@ function ReasonStrip({ it, ui }) {
1875
1956
  className: "ss-w-bi-expand",
1876
1957
  "aria-expanded": open,
1877
1958
  "aria-label": open ? ui?.hideStats ?? "Hide stats" : ui?.showStats ?? "Show stats",
1878
- onClick: () => setOpen((v) => !v),
1959
+ onClick: onToggle,
1879
1960
  children: /* @__PURE__ */ jsx13("span", { className: `ss-w-bi-arrow${open ? " ss-w-bi-arrow-open" : ""}`, "aria-hidden": "true" })
1880
1961
  }
1881
1962
  ) : null
@@ -1981,7 +2062,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1981
2062
  "\u2713 ",
1982
2063
  ui.inSlipLabel
1983
2064
  ] }) : null,
1984
- /* @__PURE__ */ jsx14(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
2065
+ /* @__PURE__ */ jsx14(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, marketDeveloperName: vm.marketDeveloperName, slip, ui })
1985
2066
  ] });
1986
2067
  }
1987
2068
  function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
@@ -3313,6 +3394,7 @@ function MonitorMatchCard({ match, promotedPins, monitor, ui, slip }) {
3313
3394
  selection,
3314
3395
  shownOdds: typeof row.market.price === "number" ? row.market.price.toFixed(2) : "",
3315
3396
  marketName: row.market.marketLabel,
3397
+ marketDeveloperName: row.market.marketDeveloperName,
3316
3398
  slip,
3317
3399
  ui
3318
3400
  }
@@ -4327,6 +4409,13 @@ function StatsWidget(props) {
4327
4409
  const injectedClient = "client" in props ? props.client : void 0;
4328
4410
  const cfg = props.config;
4329
4411
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
4412
+ const logger = useMemo2(
4413
+ () => built ? built.client.createEventLogger({
4414
+ operatorUserId: cfg?.userContext?.operatorUserId,
4415
+ anonymousUserId: cfg?.userContext?.anonymousUserId
4416
+ }) : null,
4417
+ [built?.client]
4418
+ );
4330
4419
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
4331
4420
  const rootRef = useRef8(null);
4332
4421
  const introDoneRef = useRef8(false);
@@ -4365,9 +4454,33 @@ function StatsWidget(props) {
4365
4454
  [props.config?.onAddToSlip, controller.state.conversationId, built, monitor.markInSlip]
4366
4455
  );
4367
4456
  const onDashboardTap = useMemo2(
4368
- () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
4369
- [built]
4457
+ () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input), logger) : void 0,
4458
+ [built, logger]
4370
4459
  );
4460
+ const operatorUserId = cfg?.userContext?.operatorUserId;
4461
+ const anonymousUserId = cfg?.userContext?.anonymousUserId;
4462
+ const conversationId = controller.state.conversationId ?? void 0;
4463
+ useEffect11(() => {
4464
+ if (!logger) return;
4465
+ logger.setIdentity({ operatorUserId, anonymousUserId, conversationId });
4466
+ }, [logger, operatorUserId, anonymousUserId, conversationId]);
4467
+ useEffect11(() => {
4468
+ if (!logger) return;
4469
+ const onHide = () => {
4470
+ logger.flushOnHide();
4471
+ };
4472
+ const onVisibilityHide = () => {
4473
+ if (document.visibilityState === "hidden") logger.flushOnHide();
4474
+ };
4475
+ window.addEventListener("pagehide", onHide);
4476
+ document.addEventListener("visibilitychange", onVisibilityHide);
4477
+ return () => {
4478
+ window.removeEventListener("pagehide", onHide);
4479
+ document.removeEventListener("visibilitychange", onVisibilityHide);
4480
+ logger.flushOnHide();
4481
+ logger.dispose();
4482
+ };
4483
+ }, [logger]);
4371
4484
  useEffect11(() => {
4372
4485
  if (!built || !ui.injectStyles) return;
4373
4486
  const node = rootRef.current?.getRootNode();
@@ -4375,7 +4488,7 @@ function StatsWidget(props) {
4375
4488
  }, [built, ui.injectStyles]);
4376
4489
  if (!built) return null;
4377
4490
  const attrs = themeAttrs(ui);
4378
- return /* @__PURE__ */ jsx50("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx50(ChatPanel, { controller, ui, slip, onDashboardTap, monitor: monitor.enabled ? monitor : void 0 }) : /* @__PURE__ */ jsx50(
4491
+ return /* @__PURE__ */ jsx50(EventLogContext.Provider, { value: logger, children: /* @__PURE__ */ jsx50("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx50(ChatPanel, { controller, ui, slip, onDashboardTap, monitor: monitor.enabled ? monitor : void 0 }) : /* @__PURE__ */ jsx50(
4379
4492
  ChatFab,
4380
4493
  {
4381
4494
  label: ui.launcherLabel,
@@ -4388,7 +4501,7 @@ function StatsWidget(props) {
4388
4501
  introAllowed: !introDoneRef.current,
4389
4502
  onIntroStart: markIntroDone
4390
4503
  }
4391
- ) });
4504
+ ) }) });
4392
4505
  }
4393
4506
  function emptyConversation() {
4394
4507
  return { send: () => {