@agent-native/core 0.77.6 → 0.77.7

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.
Files changed (159) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/thread-data-builder.ts +21 -2
  5. package/corpus/core/src/cli/create.ts +35 -1
  6. package/corpus/core/src/cli/skills.ts +8 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +57 -25
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
  9. package/corpus/core/src/client/analytics.ts +24 -2
  10. package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
  11. package/corpus/core/src/client/use-chat-threads.ts +104 -55
  12. package/corpus/core/src/server/analytics.ts +27 -3
  13. package/corpus/core/src/sharing/access.ts +35 -13
  14. package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
  15. package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
  16. package/corpus/core/src/sharing/registry.ts +9 -0
  17. package/corpus/core/src/vite/client.ts +3 -0
  18. package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
  19. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
  20. package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
  21. package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
  22. package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
  24. package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
  25. package/corpus/templates/analytics/netlify.toml +1 -0
  26. package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
  27. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
  28. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
  29. package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
  30. package/corpus/templates/assets/netlify.toml +1 -0
  31. package/corpus/templates/brain/netlify.toml +1 -0
  32. package/corpus/templates/calendar/app/i18n-data.ts +12 -0
  33. package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
  36. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
  37. package/corpus/templates/calendar/netlify.toml +1 -0
  38. package/corpus/templates/chat/netlify.toml +1 -0
  39. package/corpus/templates/clips/AGENTS.md +9 -7
  40. package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
  41. package/corpus/templates/clips/actions/archive-recording.ts +5 -2
  42. package/corpus/templates/clips/actions/clear-edits.ts +5 -2
  43. package/corpus/templates/clips/actions/create-folder.ts +7 -2
  44. package/corpus/templates/clips/actions/delete-folder.ts +7 -4
  45. package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
  46. package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
  47. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
  48. package/corpus/templates/clips/actions/list-notifications.ts +9 -5
  49. package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
  50. package/corpus/templates/clips/actions/list-recordings.ts +4 -1
  51. package/corpus/templates/clips/actions/move-recording.ts +5 -1
  52. package/corpus/templates/clips/actions/rename-folder.ts +6 -3
  53. package/corpus/templates/clips/actions/request-transcript.ts +5 -2
  54. package/corpus/templates/clips/actions/restore-recording.ts +5 -2
  55. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
  56. package/corpus/templates/clips/actions/set-chapters.ts +5 -2
  57. package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
  58. package/corpus/templates/clips/actions/split-recording.ts +5 -2
  59. package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
  60. package/corpus/templates/clips/actions/trash-recording.ts +5 -2
  61. package/corpus/templates/clips/actions/undo-edit.ts +5 -2
  62. package/corpus/templates/clips/actions/view-screen.ts +2 -1
  63. package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
  64. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
  65. package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
  66. package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
  67. package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
  68. package/corpus/templates/clips/chrome-extension/package.json +3 -0
  69. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  70. package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
  71. package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
  72. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
  73. package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
  74. package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
  75. package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
  76. package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
  77. package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
  78. package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
  79. package/corpus/templates/clips/netlify.toml +1 -0
  80. package/corpus/templates/clips/server/db/index.ts +1 -0
  81. package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
  82. package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
  83. package/corpus/templates/clips/server/lib/recordings.ts +18 -2
  84. package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
  85. package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
  86. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
  87. package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
  88. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
  89. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
  90. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
  91. package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
  92. package/corpus/templates/content/netlify.toml +1 -0
  93. package/corpus/templates/design/netlify.toml +1 -0
  94. package/corpus/templates/dispatch/netlify.toml +1 -0
  95. package/corpus/templates/forms/netlify.toml +1 -0
  96. package/corpus/templates/macros/netlify.toml +1 -0
  97. package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
  98. package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
  99. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
  100. package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
  101. package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
  102. package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
  103. package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
  104. package/corpus/templates/mail/netlify.toml +1 -0
  105. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
  106. package/corpus/templates/plan/netlify.toml +1 -0
  107. package/corpus/templates/slides/netlify.toml +1 -0
  108. package/corpus/templates/videos/netlify.toml +1 -0
  109. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  110. package/dist/agent/thread-data-builder.js +23 -2
  111. package/dist/agent/thread-data-builder.js.map +1 -1
  112. package/dist/cli/create.d.ts.map +1 -1
  113. package/dist/cli/create.js +28 -1
  114. package/dist/cli/create.js.map +1 -1
  115. package/dist/cli/skills.d.ts +1 -1
  116. package/dist/cli/skills.d.ts.map +1 -1
  117. package/dist/cli/skills.js +8 -0
  118. package/dist/cli/skills.js.map +1 -1
  119. package/dist/client/AgentPanel.d.ts +8 -2
  120. package/dist/client/AgentPanel.d.ts.map +1 -1
  121. package/dist/client/AgentPanel.js +29 -15
  122. package/dist/client/AgentPanel.js.map +1 -1
  123. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  124. package/dist/client/MultiTabAssistantChat.js +4 -4
  125. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  126. package/dist/client/analytics.d.ts.map +1 -1
  127. package/dist/client/analytics.js +22 -2
  128. package/dist/client/analytics.js.map +1 -1
  129. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  130. package/dist/client/use-agent-engine-configured.js +23 -13
  131. package/dist/client/use-agent-engine-configured.js.map +1 -1
  132. package/dist/client/use-chat-threads.d.ts +4 -0
  133. package/dist/client/use-chat-threads.d.ts.map +1 -1
  134. package/dist/client/use-chat-threads.js +49 -4
  135. package/dist/client/use-chat-threads.js.map +1 -1
  136. package/dist/collab/routes.d.ts +2 -2
  137. package/dist/notifications/routes.d.ts +1 -1
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/resources/handlers.d.ts +2 -2
  140. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  141. package/dist/server/analytics.d.ts +4 -0
  142. package/dist/server/analytics.d.ts.map +1 -1
  143. package/dist/server/analytics.js +19 -3
  144. package/dist/server/analytics.js.map +1 -1
  145. package/dist/server/transcribe-voice.d.ts +1 -1
  146. package/dist/sharing/access.d.ts.map +1 -1
  147. package/dist/sharing/access.js +26 -12
  148. package/dist/sharing/access.js.map +1 -1
  149. package/dist/sharing/actions/share-resource.js +20 -9
  150. package/dist/sharing/actions/share-resource.js.map +1 -1
  151. package/dist/sharing/actions/unshare-resource.js +13 -2
  152. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  153. package/dist/sharing/registry.d.ts +9 -0
  154. package/dist/sharing/registry.d.ts.map +1 -1
  155. package/dist/sharing/registry.js.map +1 -1
  156. package/dist/vite/client.d.ts.map +1 -1
  157. package/dist/vite/client.js +1 -0
  158. package/dist/vite/client.js.map +1 -1
  159. package/package.json +3 -3
@@ -26,8 +26,12 @@ function redactString(value: string): string {
26
26
  return redactBrowserDiagnosticString(value);
27
27
  }
28
28
 
29
+ function redactUrlString(value: string): string {
30
+ return redactBrowserDiagnosticString(value, { redactQueryValues: true });
31
+ }
32
+
29
33
  function sanitizeUrl(raw: string): string {
30
- const redacted = redactString(raw);
34
+ const redacted = redactUrlString(raw);
31
35
  try {
32
36
  const parsed = new URL(redacted, window.location.href);
33
37
  parsed.username = "";
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Chrome extension recordings now upload large capture bursts reliably and report extension errors to Sentry.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Console diagnostics keep more useful non-secret details when clips are shared with agents.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Fixed owner recognition so Clips created from desktop and web sessions remain editable by the same signed-in user.
@@ -10,6 +10,9 @@
10
10
  "typecheck": "tsgo --noEmit",
11
11
  "package": "pnpm build && tsx scripts/package.ts"
12
12
  },
13
+ "dependencies": {
14
+ "@sentry/browser": "^10.61.0"
15
+ },
13
16
  "devDependencies": {
14
17
  "@types/chrome": "^0.2.0",
15
18
  "@typescript/native-preview": "catalog:",
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.2",
5
+ "version": "0.1.3",
6
6
  "description": "Start Clips recordings from Chrome with optional redacted console and network diagnostics.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
@@ -1,3 +1,7 @@
1
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
2
+
3
+ initExtensionSentry("background");
4
+
1
5
  const DEFAULT_CLIPS_BASE_URL = "https://clips.agent-native.com";
2
6
  const DEBUGGER_PROTOCOL_VERSION = "1.3";
3
7
  const MAX_CONSOLE_LOGS = 400;
@@ -149,6 +153,15 @@ type OffscreenStatusMessage = {
149
153
  error?: string;
150
154
  };
151
155
 
156
+ type ExtensionErrorMessage = {
157
+ type: "CLIPS_EXTENSION_ERROR";
158
+ surface?: string;
159
+ name?: string;
160
+ message?: string;
161
+ stack?: string;
162
+ context?: Record<string, unknown>;
163
+ };
164
+
152
165
  type PendingNetworkRequest = {
153
166
  requestId: string;
154
167
  timestampMs: number;
@@ -516,19 +529,24 @@ function truncate(value: string, maxLength: number): string {
516
529
  return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
517
530
  }
518
531
 
519
- function redactString(value: string): string {
520
- return value
532
+ function redactString(
533
+ value: string,
534
+ options: { redactQueryValues?: boolean } = {},
535
+ ): string {
536
+ const redacted = value
521
537
  .replace(AUTHORIZATION_SCHEME_RE, "$1$2<redacted>")
522
538
  .replace(/\b(bearer|basic)\s+[a-z0-9._~+/-]+=*/gi, "$1 <redacted>")
523
539
  .replace(DOUBLE_QUOTED_SECRET_VALUE_RE, '$1$2$1$3"<redacted>"')
524
540
  .replace(SINGLE_QUOTED_SECRET_VALUE_RE, "$1$2$1$3'<redacted>'")
525
- .replace(UNQUOTED_SECRET_VALUE_RE, "$1$2$1$3<redacted>")
526
- .replace(/([?&][^=\s&?#]+)=([^&\s#]+)/g, "$1=<redacted>");
541
+ .replace(UNQUOTED_SECRET_VALUE_RE, "$1$2$1$3<redacted>");
542
+ return options.redactQueryValues
543
+ ? redacted.replace(/([?&][^=\s&?#]+)=([^&\s#]+)/g, "$1=<redacted>")
544
+ : redacted;
527
545
  }
528
546
 
529
547
  function sanitizeUrl(raw: string | null | undefined): string | null {
530
548
  if (!raw) return null;
531
- const redacted = redactString(raw);
549
+ const redacted = redactString(raw, { redactQueryValues: true });
532
550
  try {
533
551
  const parsed = new URL(redacted);
534
552
  parsed.username = "";
@@ -1016,6 +1034,17 @@ async function armRecording(args: {
1016
1034
  visibility: "public",
1017
1035
  });
1018
1036
  } catch (err) {
1037
+ captureExtensionError(err, {
1038
+ tags: {
1039
+ surface: "background",
1040
+ recordingStep: "create-recording",
1041
+ },
1042
+ extra: {
1043
+ captureSurface: settings.captureSurface,
1044
+ includeCamera: settings.includeCamera,
1045
+ includeMicrophone: settings.includeMicrophone,
1046
+ },
1047
+ });
1019
1048
  await abortArming();
1020
1049
  throw err;
1021
1050
  }
@@ -1070,6 +1099,18 @@ async function armRecording(args: {
1070
1099
  });
1071
1100
  } catch (err) {
1072
1101
  console.error("[clips-bg] arm: BEGIN failed", err);
1102
+ captureExtensionError(err, {
1103
+ tags: {
1104
+ surface: "background",
1105
+ recordingStep: "offscreen-begin",
1106
+ },
1107
+ extra: {
1108
+ recordingId: created.id,
1109
+ captureSurface: settings.captureSurface,
1110
+ includeCamera: settings.includeCamera,
1111
+ includeMicrophone: settings.includeMicrophone,
1112
+ },
1113
+ });
1073
1114
  await cancelRecording();
1074
1115
  throw err;
1075
1116
  }
@@ -1553,9 +1594,15 @@ function pushConsole(
1553
1594
  const timestampMs = Number.isFinite(entry.timestampMs)
1554
1595
  ? (entry.timestampMs as number)
1555
1596
  : nowMs();
1556
- const message = truncate(redactString(entry.message), MAX_MESSAGE_LENGTH);
1597
+ const message = truncate(
1598
+ redactString(entry.message, { redactQueryValues: true }),
1599
+ MAX_MESSAGE_LENGTH,
1600
+ );
1557
1601
  const stack = entry.stack
1558
- ? truncate(redactString(entry.stack), MAX_MESSAGE_LENGTH)
1602
+ ? truncate(
1603
+ redactString(entry.stack, { redactQueryValues: true }),
1604
+ MAX_MESSAGE_LENGTH,
1605
+ )
1559
1606
  : "";
1560
1607
  session.consoleLogs.push({
1561
1608
  timestampMs,
@@ -1762,7 +1809,10 @@ function handleResponseReceived(
1762
1809
  pending.ok = response.status >= 200 && response.status < 400;
1763
1810
  }
1764
1811
  if (typeof response.statusText === "string") {
1765
- pending.statusText = truncate(redactString(response.statusText), 120);
1812
+ pending.statusText = truncate(
1813
+ redactString(response.statusText, { redactQueryValues: true }),
1814
+ 120,
1815
+ );
1766
1816
  }
1767
1817
  }
1768
1818
 
@@ -1792,7 +1842,12 @@ function finalizeNetworkRequest(
1792
1842
  ...(typeof pending.ok === "boolean" ? { ok: pending.ok } : {}),
1793
1843
  durationMs: Math.round(durationMs),
1794
1844
  ...(error
1795
- ? { error: truncate(redactString(error), MAX_MESSAGE_LENGTH) }
1845
+ ? {
1846
+ error: truncate(
1847
+ redactString(error, { redactQueryValues: true }),
1848
+ MAX_MESSAGE_LENGTH,
1849
+ ),
1850
+ }
1796
1851
  : {}),
1797
1852
  });
1798
1853
  }
@@ -1923,6 +1978,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1923
1978
  (message as { type?: unknown })?.type,
1924
1979
  err,
1925
1980
  );
1981
+ captureExtensionError(err, {
1982
+ tags: {
1983
+ surface: "background",
1984
+ messageType: String((message as { type?: unknown })?.type ?? ""),
1985
+ },
1986
+ });
1926
1987
  response = {
1927
1988
  ok: false,
1928
1989
  error: err instanceof Error ? err.message : "Could not use Clips.",
@@ -1940,6 +2001,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1940
2001
  async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
1941
2002
  const type = (message as { type?: unknown }).type;
1942
2003
 
2004
+ if (type === "CLIPS_EXTENSION_ERROR") {
2005
+ const report = message as ExtensionErrorMessage;
2006
+ const error = new Error(report.message || "Chrome extension error");
2007
+ error.name = report.name || "ExtensionError";
2008
+ if (report.stack) error.stack = report.stack;
2009
+ captureExtensionError(error, {
2010
+ tags: {
2011
+ surface: report.surface || "content-script",
2012
+ mechanism: "content-script-report",
2013
+ },
2014
+ extra: report.context,
2015
+ });
2016
+ return { ok: true };
2017
+ }
2018
+
1943
2019
  // Status updates streamed from the offscreen recorder.
1944
2020
  if (type === "CLIPS_NATIVE_STATUS") {
1945
2021
  const status = message as OffscreenStatusMessage;
@@ -18,6 +18,61 @@
18
18
  const ALL_PARTS: OverlayPart[] = ["bubble", "countdown", "toolbar", "saving"];
19
19
  const flags = window as unknown as { __clipsOverlayHostReady?: boolean };
20
20
 
21
+ function errorPayload(error: unknown): {
22
+ name: string;
23
+ message: string;
24
+ stack?: string;
25
+ } {
26
+ if (error instanceof Error) {
27
+ return {
28
+ name: error.name || "Error",
29
+ message: error.message || "Unknown content-script error",
30
+ stack: error.stack,
31
+ };
32
+ }
33
+ return {
34
+ name: "Error",
35
+ message: String(error ?? "Unknown content-script error"),
36
+ };
37
+ }
38
+
39
+ function reportContentScriptError(
40
+ error: unknown,
41
+ context: Record<string, unknown> = {},
42
+ ): void {
43
+ try {
44
+ chrome.runtime.sendMessage(
45
+ {
46
+ type: "CLIPS_EXTENSION_ERROR",
47
+ surface: "content-script",
48
+ ...errorPayload(error),
49
+ context: {
50
+ ...context,
51
+ pageUrl: location.href,
52
+ },
53
+ },
54
+ () => void chrome.runtime.lastError,
55
+ );
56
+ } catch {
57
+ /* background unavailable */
58
+ }
59
+ }
60
+
61
+ window.addEventListener("error", (event) => {
62
+ reportContentScriptError(event.error || event.message, {
63
+ mechanism: "global-error",
64
+ filename: event.filename,
65
+ lineno: event.lineno,
66
+ colno: event.colno,
67
+ });
68
+ });
69
+
70
+ window.addEventListener("unhandledrejection", (event) => {
71
+ reportContentScriptError(event.reason, {
72
+ mechanism: "unhandled-rejection",
73
+ });
74
+ });
75
+
21
76
  // ----- Draggable, resizable camera bubble ---------------------------------
22
77
  // Size + position persist in storage so the bubble stays where the user put it
23
78
  // across pages and recordings (like the desktop app). The iframe can't move or
@@ -19,6 +19,10 @@
19
19
  import { scheduleReadyChime } from "@shared/recording-audio";
20
20
  import { chunkUploadUrl, pickMimeType } from "@shared/recording-core";
21
21
 
22
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
23
+
24
+ initExtensionSentry("offscreen");
25
+
22
26
  type CaptureMode = "screen" | "camera";
23
27
 
24
28
  type AcquireMessage = {
@@ -118,6 +122,8 @@ type ActiveRecording = {
118
122
  rejectStopped: (error: Error) => void;
119
123
  };
120
124
 
125
+ const UPLOAD_SLICE_BYTES = 3 * 1024 * 1024;
126
+
121
127
  let prepared: PreparedStreams | null = null;
122
128
  let activeRecording: ActiveRecording | null = null;
123
129
 
@@ -399,13 +405,46 @@ async function uploadChunk(
399
405
  Boolean(recording.authToken),
400
406
  text.slice(0, 200),
401
407
  );
402
- throw new Error(
408
+ const error = new Error(
403
409
  data?.error || `Upload failed (${res.status}): ${text || res.statusText}`,
404
410
  );
411
+ captureExtensionError(error, {
412
+ tags: {
413
+ surface: "offscreen",
414
+ recordingStep: "chunk-upload",
415
+ httpStatus: String(res.status),
416
+ isFinal: extra.isFinal ? "true" : "false",
417
+ },
418
+ extra: {
419
+ recordingId: recording.recordingId,
420
+ chunkIndex: index,
421
+ chunkBytes: blob.size,
422
+ total: extra.total,
423
+ mimeType: blob.type || recording.mimeType,
424
+ responseBodyTail: text.slice(0, 2000),
425
+ hadAuth: Boolean(recording.authToken),
426
+ },
427
+ });
428
+ throw error;
405
429
  }
406
430
  return data;
407
431
  }
408
432
 
433
+ async function uploadBlobInSlices(
434
+ recording: ActiveRecording,
435
+ blob: Blob,
436
+ ): Promise<void> {
437
+ const totalSlices = Math.max(1, Math.ceil(blob.size / UPLOAD_SLICE_BYTES));
438
+ for (let sliceIndex = 0; sliceIndex < totalSlices; sliceIndex += 1) {
439
+ if (recording.cancelled || recording.uploadFailure) return;
440
+ const start = sliceIndex * UPLOAD_SLICE_BYTES;
441
+ const end = Math.min(start + UPLOAD_SLICE_BYTES, blob.size);
442
+ const slice = blob.slice(start, end, blob.type || recording.mimeType);
443
+ const index = recording.chunkIndex++;
444
+ await uploadChunk(recording, slice, index);
445
+ }
446
+ }
447
+
409
448
  function stopStreams(streams: (MediaStream | null)[]): void {
410
449
  for (const stream of streams) {
411
450
  if (!stream) continue;
@@ -616,14 +655,25 @@ async function begin(message: BeginMessage): Promise<{
616
655
  ) {
617
656
  return;
618
657
  }
619
- const index = recording.chunkIndex++;
620
658
  // Record the failure and stop, but do NOT re-throw: re-throwing leaves a
621
659
  // rejected promise that surfaces as an "Uncaught (in promise)" error (bad
622
660
  // look in a Chrome Web Store review). finalizeStop reads recording.upload-
623
661
  // Failure and surfaces it through the normal error path instead.
624
- const upload = uploadChunk(recording, event.data, index).catch((err) => {
662
+ const upload = uploadBlobInSlices(recording, event.data).catch((err) => {
625
663
  recording.uploadFailure =
626
664
  err instanceof Error ? err : new Error(String(err));
665
+ captureExtensionError(recording.uploadFailure, {
666
+ tags: {
667
+ surface: "offscreen",
668
+ recordingStep: "dataavailable-upload",
669
+ },
670
+ extra: {
671
+ recordingId: recording.recordingId,
672
+ blobBytes: event.data.size,
673
+ chunkIndex: recording.chunkIndex,
674
+ mimeType: event.data.type || recording.mimeType,
675
+ },
676
+ });
627
677
  reportStatus(recording.sessionId, "error", {
628
678
  error: recording.uploadFailure.message,
629
679
  });
@@ -687,6 +737,20 @@ function startRecorderNow(recording: ActiveRecording): void {
687
737
  hasCamera: recording.hasCamera,
688
738
  });
689
739
  } catch (err) {
740
+ captureExtensionError(err, {
741
+ tags: {
742
+ surface: "offscreen",
743
+ recordingStep: "recorder-start",
744
+ },
745
+ extra: {
746
+ recordingId: recording.recordingId,
747
+ mimeType: recording.mimeType,
748
+ width: recording.dimensions.width,
749
+ height: recording.dimensions.height,
750
+ hasAudio: recording.hasAudio,
751
+ hasCamera: recording.hasCamera,
752
+ },
753
+ });
690
754
  reportStatus(recording.sessionId, "error", {
691
755
  recordingId: recording.recordingId,
692
756
  error: err instanceof Error ? err.message : "Could not start recording.",
@@ -766,6 +830,18 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
766
830
  cleanup(recording);
767
831
  if (activeRecording === recording) activeRecording = null;
768
832
  const error = err instanceof Error ? err : new Error(String(err));
833
+ captureExtensionError(error, {
834
+ tags: {
835
+ surface: "offscreen",
836
+ recordingStep: "finalize-stop",
837
+ },
838
+ extra: {
839
+ recordingId: recording.recordingId,
840
+ chunkCount: recording.chunkIndex,
841
+ mimeType: recording.mimeType,
842
+ durationMs: Math.max(0, Date.now() - recording.startedAtMs),
843
+ },
844
+ });
769
845
  reportStatus(recording.sessionId, "error", {
770
846
  recordingId: recording.recordingId,
771
847
  error: error.message,
@@ -1,3 +1,7 @@
1
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
2
+
3
+ initExtensionSentry("options");
4
+
1
5
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
2
6
 
3
7
  type ExtensionSettings = {
@@ -116,6 +120,9 @@ async function init(): Promise<void> {
116
120
  await saveSettings(settings);
117
121
  setStatus("Settings saved.");
118
122
  } catch (err) {
123
+ captureExtensionError(err, {
124
+ tags: { surface: "options", action: "save-settings" },
125
+ });
119
126
  setStatus(
120
127
  err instanceof Error ? err.message : "Could not save settings.",
121
128
  "error",
@@ -132,6 +139,9 @@ async function init(): Promise<void> {
132
139
  }
133
140
 
134
141
  void init().catch((err) => {
142
+ captureExtensionError(err, {
143
+ tags: { surface: "options", action: "init" },
144
+ });
135
145
  setStatus(
136
146
  err instanceof Error ? err.message : "Could not load settings.",
137
147
  "error",
@@ -1,4 +1,7 @@
1
1
  import "./overlay.css";
2
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
3
+
4
+ initExtensionSentry("overlay");
2
5
 
3
6
  // The overlay runs as an extension-origin iframe injected into the page by the
4
7
  // content script. Each iframe renders one "part" of the Loom-style recording UI
@@ -166,6 +169,9 @@ async function initBubble(): Promise<void> {
166
169
  postBubble("camera-ready");
167
170
  } catch (err) {
168
171
  console.warn("[clips-overlay] camera getUserMedia failed:", err);
172
+ captureExtensionError(err, {
173
+ tags: { surface: "overlay", overlayPart: "bubble" },
174
+ });
169
175
  const empty = document.createElement("div");
170
176
  empty.className = "bubble-empty";
171
177
  empty.innerHTML = ICONS.cameraOff;
@@ -5,6 +5,10 @@
5
5
  // the grant for the whole chrome-extension:// origin — so the offscreen recorder
6
6
  // (mic) and the camera-bubble iframe both work afterward.
7
7
 
8
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
9
+
10
+ initExtensionSentry("permission");
11
+
8
12
  const enableBtn = document.getElementById("enable") as HTMLButtonElement;
9
13
  const statusEl = document.getElementById("status") as HTMLDivElement;
10
14
  const successEl = document.getElementById("success-state") as HTMLDivElement;
@@ -71,7 +75,10 @@ async function requestOne(kind: "mic" | "cam"): Promise<boolean> {
71
75
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
72
76
  for (const track of stream.getTracks()) track.stop();
73
77
  return true;
74
- } catch {
78
+ } catch (err) {
79
+ captureExtensionError(err, {
80
+ tags: { surface: "permission", permission: kind },
81
+ });
75
82
  return false;
76
83
  }
77
84
  }
@@ -1,3 +1,7 @@
1
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
2
+
3
+ initExtensionSentry("popup");
4
+
1
5
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
2
6
  type RecordingModeChoice = "screen-camera" | "screen" | "camera";
3
7
 
@@ -1032,6 +1036,9 @@ async function init(): Promise<void> {
1032
1036
  1400,
1033
1037
  );
1034
1038
  } catch (err) {
1039
+ captureExtensionError(err, {
1040
+ tags: { surface: "popup", action: "submit-feedback" },
1041
+ });
1035
1042
  feedbackSubmit.disabled = !feedbackTextarea.value.trim();
1036
1043
  feedbackSubmit.textContent = FEEDBACK_SUBMIT_TEXT;
1037
1044
  feedbackHint.textContent =
@@ -1065,51 +1072,67 @@ async function init(): Promise<void> {
1065
1072
  start.disabled = true;
1066
1073
  signIn.hidden = true;
1067
1074
  setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
1068
- authStatus = await readAuthStatus(settings);
1069
- if (authStatus === "signed-out") {
1075
+ try {
1076
+ authStatus = await readAuthStatus(settings);
1077
+ if (authStatus === "signed-out") {
1078
+ start.disabled = false;
1079
+ start.hidden = true;
1080
+ signIn.hidden = false;
1081
+ setStatus("");
1082
+ return;
1083
+ }
1084
+ const storageConfigured = await readVideoStorageConfigured(settings);
1085
+ if (!storageConfigured) {
1086
+ start.disabled = false;
1087
+ setStatus(
1088
+ "Connect Builder.io or S3 storage in Clips, then start recording.",
1089
+ "error",
1090
+ );
1091
+ await createTab(`${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`);
1092
+ window.close();
1093
+ return;
1094
+ }
1095
+ // Gate at record time: if the user wants camera/mic but hasn't granted the
1096
+ // extension access yet, send them to the onboarding page first. Requesting
1097
+ // there (a real extension page) is the only place Chrome reliably shows the
1098
+ // permission dialog and persists the grant for the offscreen recorder + bubble.
1099
+ if (!(await ensureMediaPermission(settings))) {
1100
+ start.disabled = false;
1101
+ setStatus("Allow camera & microphone, then start recording.", "error");
1102
+ await createTab(chrome.runtime.getURL("src/permission.html"));
1103
+ window.close();
1104
+ return;
1105
+ }
1106
+ await saveSettings(settings);
1107
+ const response = await sendStartMessage(settings);
1108
+ if (response.ok) {
1109
+ window.close();
1110
+ return;
1111
+ }
1070
1112
  start.disabled = false;
1071
- start.hidden = true;
1072
- signIn.hidden = false;
1073
- setStatus("");
1074
- return;
1075
- }
1076
- const storageConfigured = await readVideoStorageConfigured(settings);
1077
- if (!storageConfigured) {
1113
+ const message = response.error || "Could not start Clips.";
1114
+ if (isSignInError(message)) {
1115
+ start.hidden = true;
1116
+ signIn.hidden = false;
1117
+ setStatus("");
1118
+ return;
1119
+ }
1120
+ setStatus(message, "error");
1121
+ } catch (err) {
1122
+ captureExtensionError(err, {
1123
+ tags: { surface: "popup", action: "start-recording" },
1124
+ extra: {
1125
+ captureSurface: settings.captureSurface,
1126
+ includeCamera: settings.includeCamera,
1127
+ includeMicrophone: settings.includeMicrophone,
1128
+ },
1129
+ });
1078
1130
  start.disabled = false;
1079
1131
  setStatus(
1080
- "Connect Builder.io or S3 storage in Clips, then start recording.",
1132
+ err instanceof Error ? err.message : "Could not start Clips.",
1081
1133
  "error",
1082
1134
  );
1083
- await createTab(`${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`);
1084
- window.close();
1085
- return;
1086
- }
1087
- // Gate at record time: if the user wants camera/mic but hasn't granted the
1088
- // extension access yet, send them to the onboarding page first. Requesting
1089
- // there (a real extension page) is the only place Chrome reliably shows the
1090
- // permission dialog and persists the grant for the offscreen recorder + bubble.
1091
- if (!(await ensureMediaPermission(settings))) {
1092
- start.disabled = false;
1093
- setStatus("Allow camera & microphone, then start recording.", "error");
1094
- await createTab(chrome.runtime.getURL("src/permission.html"));
1095
- window.close();
1096
- return;
1097
1135
  }
1098
- await saveSettings(settings);
1099
- const response = await sendStartMessage(settings);
1100
- if (response.ok) {
1101
- window.close();
1102
- return;
1103
- }
1104
- start.disabled = false;
1105
- const message = response.error || "Could not start Clips.";
1106
- if (isSignInError(message)) {
1107
- start.hidden = true;
1108
- signIn.hidden = false;
1109
- setStatus("");
1110
- return;
1111
- }
1112
- setStatus(message, "error");
1113
1136
  });
1114
1137
 
1115
1138
  signIn.addEventListener("click", async () => {
@@ -1173,6 +1196,9 @@ async function init(): Promise<void> {
1173
1196
  }
1174
1197
 
1175
1198
  void init().catch((err) => {
1199
+ captureExtensionError(err, {
1200
+ tags: { surface: "popup", action: "init" },
1201
+ });
1176
1202
  setStatus(
1177
1203
  err instanceof Error ? err.message : "Could not load popup.",
1178
1204
  "error",