@agent-native/core 0.79.21 → 0.79.22

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 (131) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +10 -0
  3. package/corpus/core/docs/content/deployment.md +82 -82
  4. package/corpus/core/docs/content/faq.md +1 -1
  5. package/corpus/core/docs/content/tracking.md +3 -3
  6. package/corpus/core/package.json +1 -1
  7. package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
  8. package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
  9. package/corpus/core/src/agent/engine/registry.ts +59 -11
  10. package/corpus/core/src/client/FeedbackButton.tsx +11 -1
  11. package/corpus/core/src/client/agent-engine-key.ts +57 -10
  12. package/corpus/core/src/client/analytics.ts +78 -27
  13. package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
  14. package/corpus/core/src/client/index.ts +2 -0
  15. package/corpus/core/src/client/session-replay.ts +159 -17
  16. package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
  17. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
  18. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
  19. package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
  20. package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
  21. package/corpus/core/src/server/core-routes-plugin.ts +26 -2
  22. package/corpus/core/src/styles/blocks.css +23 -7
  23. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
  24. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
  25. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
  29. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
  33. package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
  35. package/corpus/templates/analytics/netlify.toml +4 -0
  36. package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
  37. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
  38. package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
  39. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
  40. package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
  41. package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
  42. package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
  43. package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +1 -0
  44. package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +1 -0
  46. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  47. package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
  48. package/corpus/templates/clips/desktop/package.json +1 -0
  49. package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
  50. package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
  51. package/corpus/templates/clips/desktop/src/main.tsx +2 -0
  52. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
  53. package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
  54. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  55. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
  56. package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
  57. package/corpus/templates/clips/desktop/vite.config.ts +66 -0
  58. package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
  59. package/corpus/templates/forms/actions/export-responses.ts +3 -2
  60. package/corpus/templates/forms/actions/list-responses.ts +2 -1
  61. package/corpus/templates/forms/actions/response-insights.ts +13 -4
  62. package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
  63. package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
  64. package/corpus/templates/forms/server/lib/integrations.ts +9 -6
  65. package/corpus/templates/forms/shared/submitter-email.ts +20 -0
  66. package/corpus/templates/forms/shared/types.ts +1 -1
  67. package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
  68. package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
  69. package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
  70. package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
  71. package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
  72. package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
  73. package/dist/agent/engine/provider-env-vars.d.ts +1 -0
  74. package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
  75. package/dist/agent/engine/provider-env-vars.js +1 -0
  76. package/dist/agent/engine/provider-env-vars.js.map +1 -1
  77. package/dist/agent/engine/registry.d.ts.map +1 -1
  78. package/dist/agent/engine/registry.js +39 -12
  79. package/dist/agent/engine/registry.js.map +1 -1
  80. package/dist/client/FeedbackButton.d.ts.map +1 -1
  81. package/dist/client/FeedbackButton.js +6 -1
  82. package/dist/client/FeedbackButton.js.map +1 -1
  83. package/dist/client/agent-engine-key.d.ts +14 -0
  84. package/dist/client/agent-engine-key.d.ts.map +1 -1
  85. package/dist/client/agent-engine-key.js +35 -10
  86. package/dist/client/agent-engine-key.js.map +1 -1
  87. package/dist/client/analytics.d.ts.map +1 -1
  88. package/dist/client/analytics.js +71 -24
  89. package/dist/client/analytics.js.map +1 -1
  90. package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
  91. package/dist/client/blocks/library/HighlightedCode.js +1 -1
  92. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  93. package/dist/client/index.d.ts +1 -1
  94. package/dist/client/index.d.ts.map +1 -1
  95. package/dist/client/index.js +1 -1
  96. package/dist/client/index.js.map +1 -1
  97. package/dist/client/session-replay.d.ts +1 -1
  98. package/dist/client/session-replay.d.ts.map +1 -1
  99. package/dist/client/session-replay.js +115 -17
  100. package/dist/client/session-replay.js.map +1 -1
  101. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  102. package/dist/client/settings/SettingsPanel.js +42 -6
  103. package/dist/client/settings/SettingsPanel.js.map +1 -1
  104. package/dist/collab/awareness.d.ts +2 -2
  105. package/dist/collab/awareness.d.ts.map +1 -1
  106. package/dist/collab/routes.d.ts +1 -1
  107. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  108. package/dist/notifications/routes.d.ts +2 -2
  109. package/dist/observability/routes.d.ts +7 -7
  110. package/dist/progress/routes.d.ts +1 -1
  111. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  112. package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
  113. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  114. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  115. package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
  116. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  117. package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
  118. package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
  119. package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
  120. package/dist/server/agent-engine-api-key-route.d.ts +4 -4
  121. package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
  122. package/dist/server/agent-engine-api-key-route.js +71 -11
  123. package/dist/server/agent-engine-api-key-route.js.map +1 -1
  124. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  125. package/dist/server/core-routes-plugin.js +20 -3
  126. package/dist/server/core-routes-plugin.js.map +1 -1
  127. package/dist/styles/blocks.css +23 -7
  128. package/docs/content/deployment.md +82 -82
  129. package/docs/content/faq.md +1 -1
  130. package/docs/content/tracking.md +3 -3
  131. package/package.json +1 -1
@@ -17,6 +17,7 @@
17
17
  "dependencies": {
18
18
  "@radix-ui/react-popover": "^1.1.15",
19
19
  "@radix-ui/react-tooltip": "^1.2.8",
20
+ "@sentry/browser": "10.62.0",
20
21
  "@tabler/icons-react": "^3.26.0",
21
22
  "@tauri-apps/api": "^2.11.0",
22
23
  "@tauri-apps/plugin-fs": "2.5.1",
@@ -14,6 +14,7 @@ const RED = "\x1b[31m";
14
14
  const YELLOW = "\x1b[33m";
15
15
  const CYAN = "\x1b[36m";
16
16
  const DIM = "\x1b[2m";
17
+ const MIN_RUST_VERSION = [1, 88, 0];
17
18
 
18
19
  function has(cmd) {
19
20
  try {
@@ -107,3 +108,47 @@ if (!has("cargo")) {
107
108
 
108
109
  fail(installLines);
109
110
  }
111
+
112
+ function parseRustVersion(output) {
113
+ const match = output.match(/rustc\s+(\d+)\.(\d+)\.(\d+)/);
114
+ if (!match) return null;
115
+ return match.slice(1, 4).map((part) => Number(part));
116
+ }
117
+
118
+ function compareVersions(a, b) {
119
+ for (let i = 0; i < 3; i += 1) {
120
+ if (a[i] > b[i]) return 1;
121
+ if (a[i] < b[i]) return -1;
122
+ }
123
+ return 0;
124
+ }
125
+
126
+ let rustcOutput = "";
127
+ try {
128
+ rustcOutput = execFileSync("rustc", ["--version"], {
129
+ encoding: "utf8",
130
+ });
131
+ } catch {
132
+ fail([
133
+ "",
134
+ `${YELLOW}Cargo is installed, but ${BOLD}rustc${RESET}${YELLOW} is not available.${RESET}`,
135
+ "",
136
+ `${BOLD}Fix:${RESET} update your Rust toolchain, then retry:`,
137
+ "",
138
+ ` ${CYAN}rustup update stable${RESET}`,
139
+ ]);
140
+ }
141
+
142
+ const rustVersion = parseRustVersion(rustcOutput);
143
+ if (!rustVersion || compareVersions(rustVersion, MIN_RUST_VERSION) < 0) {
144
+ fail([
145
+ "",
146
+ `${YELLOW}Clips Desktop needs Rust ${BOLD}${MIN_RUST_VERSION.join(".")}+${RESET}${YELLOW} for the current Tauri/Sentry native SDKs.${RESET}`,
147
+ "",
148
+ `Found: ${DIM}${rustcOutput.trim() || "unknown"}${RESET}`,
149
+ "",
150
+ `${BOLD}Fix:${RESET} update Rust, then retry:`,
151
+ "",
152
+ ` ${CYAN}rustup update stable${RESET}`,
153
+ ]);
154
+ }
@@ -0,0 +1,71 @@
1
+ import * as Sentry from "@sentry/browser";
2
+
3
+ declare const __CLIPS_DESKTOP_SENTRY_DSN__: string;
4
+ declare const __CLIPS_DESKTOP_SENTRY_ENVIRONMENT__: string;
5
+ declare const __CLIPS_DESKTOP_VERSION__: string;
6
+
7
+ let initialized = false;
8
+
9
+ function configuredDsn(): string {
10
+ return typeof __CLIPS_DESKTOP_SENTRY_DSN__ === "string"
11
+ ? __CLIPS_DESKTOP_SENTRY_DSN__.trim()
12
+ : "";
13
+ }
14
+
15
+ function configuredEnvironment(): string {
16
+ return typeof __CLIPS_DESKTOP_SENTRY_ENVIRONMENT__ === "string" &&
17
+ __CLIPS_DESKTOP_SENTRY_ENVIRONMENT__
18
+ ? __CLIPS_DESKTOP_SENTRY_ENVIRONMENT__
19
+ : "production";
20
+ }
21
+
22
+ function scrubUrl(value: string): string {
23
+ try {
24
+ const url = new URL(value);
25
+ for (const key of [...url.searchParams.keys()]) {
26
+ url.searchParams.set(key, "<redacted>");
27
+ }
28
+ return url.toString();
29
+ } catch {
30
+ return value;
31
+ }
32
+ }
33
+
34
+ function scrubEvent<T extends Sentry.Event>(event: T): T {
35
+ if (event.request?.url) {
36
+ event.request.url = scrubUrl(event.request.url);
37
+ }
38
+ if (event.user) {
39
+ delete (event.user as Record<string, unknown>).ip_address;
40
+ }
41
+ return event;
42
+ }
43
+
44
+ export function initDesktopSentry(route: string): void {
45
+ if (initialized) return;
46
+ const dsn = configuredDsn();
47
+ if (!dsn) return;
48
+ initialized = true;
49
+
50
+ Sentry.init({
51
+ dsn,
52
+ environment: configuredEnvironment(),
53
+ release: `clips-desktop@${__CLIPS_DESKTOP_VERSION__ || "0.0.0"}`,
54
+ sendDefaultPii: false,
55
+ beforeSend(event) {
56
+ event.tags = {
57
+ ...event.tags,
58
+ app: "agent-native-clips",
59
+ template: "clips",
60
+ runtime: "tauri-webview",
61
+ surface: route,
62
+ };
63
+ return scrubEvent(event);
64
+ },
65
+ });
66
+
67
+ Sentry.setTag("app", "agent-native-clips");
68
+ Sentry.setTag("template", "clips");
69
+ Sentry.setTag("runtime", "tauri-webview");
70
+ Sentry.setTag("surface", route);
71
+ }
@@ -3,6 +3,7 @@ import React from "react";
3
3
  import ReactDOM from "react-dom/client";
4
4
 
5
5
  import { App } from "./app";
6
+ import { initDesktopSentry } from "./lib/sentry";
6
7
  import { Bubble } from "./overlays/bubble";
7
8
  import { Countdown } from "./overlays/countdown";
8
9
  import { Finalizing } from "./overlays/finalizing";
@@ -211,6 +212,7 @@ const rootEl = document.getElementById("root");
211
212
  if (rootEl) {
212
213
  const route = currentRoute();
213
214
  installRouteAttributes(route);
215
+ initDesktopSentry(route);
214
216
  installConsoleCapture(route);
215
217
  installBeforeUnloadCleanup();
216
218
  installHeapDebugLog();
@@ -5,7 +5,7 @@ description = "Clips — menu-bar tray app (Tauri 2)"
5
5
  authors = ["Clips"]
6
6
  license = "MIT"
7
7
  edition = "2021"
8
- rust-version = "1.77.2"
8
+ rust-version = "1.88"
9
9
 
10
10
  # See https://doc.rust-lang.org/cargo/reference/manifest.html
11
11
 
@@ -44,6 +44,7 @@ tauri-plugin-fs = "2.5.1"
44
44
  tauri-plugin-notification = "2"
45
45
  serde = { version = "1", features = ["derive"] }
46
46
  serde_json = "1"
47
+ sentry = "0.48.3"
47
48
  url = "2"
48
49
  # Used by meetings_watcher.rs to poll the backend on a background tokio
49
50
  # task. Tauri 2 already pulls reqwest in transitively, but we declare it
@@ -2,10 +2,39 @@ use std::path::Path;
2
2
  use std::process::Command;
3
3
 
4
4
  fn main() {
5
+ emit_sentry_env_reruns();
5
6
  add_swift_runtime_rpaths();
6
7
  tauri_build::build()
7
8
  }
8
9
 
10
+ fn emit_sentry_env_reruns() {
11
+ for name in [
12
+ "CLIPS_DESKTOP_SENTRY_DSN",
13
+ "TAURI_SENTRY_DSN",
14
+ "SENTRY_DESKTOP_DSN",
15
+ "SENTRY_CLIENT_DSN",
16
+ "VITE_SENTRY_CLIENT_DSN",
17
+ "VITE_SENTRY_DSN",
18
+ "SENTRY_DSN",
19
+ "CLIPS_DESKTOP_SENTRY_CLIENT_KEY",
20
+ "SENTRY_CLIENT_KEY",
21
+ "VITE_SENTRY_CLIENT_KEY",
22
+ "CLIPS_DESKTOP_SENTRY_PROJECT_ID",
23
+ "SENTRY_PROJECT_ID",
24
+ "VITE_SENTRY_PROJECT_ID",
25
+ "CLIPS_DESKTOP_SENTRY_INGEST_HOST",
26
+ "SENTRY_INGEST_HOST",
27
+ "VITE_SENTRY_INGEST_HOST",
28
+ "CLIPS_DESKTOP_SENTRY_ENVIRONMENT",
29
+ "SENTRY_ENVIRONMENT",
30
+ "NETLIFY_CONTEXT",
31
+ "VERCEL_ENV",
32
+ "NODE_ENV",
33
+ ] {
34
+ println!("cargo:rerun-if-env-changed={name}");
35
+ }
36
+ }
37
+
9
38
  fn add_swift_runtime_rpaths() {
10
39
  if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") {
11
40
  return;
@@ -16,6 +16,7 @@ mod native_speech;
16
16
  mod notifications;
17
17
  mod permission_status;
18
18
  mod recording_indicator;
19
+ mod sentry_report;
19
20
  mod shortcuts;
20
21
  mod silence_detector;
21
22
  mod state;
@@ -46,6 +47,8 @@ pub(crate) const TRAY_PNG: &[u8] = include_bytes!("../icons/tray.png");
46
47
 
47
48
  #[cfg_attr(mobile, tauri::mobile_entry_point)]
48
49
  pub fn run() {
50
+ sentry_report::init();
51
+
49
52
  tauri::Builder::default()
50
53
  .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
51
54
  // Second launch just focuses the popover of the already-running
@@ -98,6 +98,7 @@ fn install_panic_hook() {
98
98
  append_line(&path, &format!("[clips-tray] panic: {info}"));
99
99
  }
100
100
  prev(info);
101
+ crate::sentry_report::flush(std::time::Duration::from_secs(2));
101
102
  }));
102
103
  }
103
104
 
@@ -0,0 +1,142 @@
1
+ use std::sync::OnceLock;
2
+ use std::time::Duration;
3
+
4
+ static SENTRY_GUARD: OnceLock<&'static sentry::ClientInitGuard> = OnceLock::new();
5
+
6
+ const DIRECT_DSN_ENV: [(&str, Option<&str>); 7] = [
7
+ (
8
+ "CLIPS_DESKTOP_SENTRY_DSN",
9
+ option_env!("CLIPS_DESKTOP_SENTRY_DSN"),
10
+ ),
11
+ ("TAURI_SENTRY_DSN", option_env!("TAURI_SENTRY_DSN")),
12
+ ("SENTRY_DESKTOP_DSN", option_env!("SENTRY_DESKTOP_DSN")),
13
+ ("SENTRY_CLIENT_DSN", option_env!("SENTRY_CLIENT_DSN")),
14
+ (
15
+ "VITE_SENTRY_CLIENT_DSN",
16
+ option_env!("VITE_SENTRY_CLIENT_DSN"),
17
+ ),
18
+ ("VITE_SENTRY_DSN", option_env!("VITE_SENTRY_DSN")),
19
+ ("SENTRY_DSN", option_env!("SENTRY_DSN")),
20
+ ];
21
+
22
+ const KEY_ENV: [(&str, Option<&str>); 3] = [
23
+ (
24
+ "CLIPS_DESKTOP_SENTRY_CLIENT_KEY",
25
+ option_env!("CLIPS_DESKTOP_SENTRY_CLIENT_KEY"),
26
+ ),
27
+ ("SENTRY_CLIENT_KEY", option_env!("SENTRY_CLIENT_KEY")),
28
+ (
29
+ "VITE_SENTRY_CLIENT_KEY",
30
+ option_env!("VITE_SENTRY_CLIENT_KEY"),
31
+ ),
32
+ ];
33
+
34
+ const PROJECT_ENV: [(&str, Option<&str>); 3] = [
35
+ (
36
+ "CLIPS_DESKTOP_SENTRY_PROJECT_ID",
37
+ option_env!("CLIPS_DESKTOP_SENTRY_PROJECT_ID"),
38
+ ),
39
+ ("SENTRY_PROJECT_ID", option_env!("SENTRY_PROJECT_ID")),
40
+ (
41
+ "VITE_SENTRY_PROJECT_ID",
42
+ option_env!("VITE_SENTRY_PROJECT_ID"),
43
+ ),
44
+ ];
45
+
46
+ const HOST_ENV: [(&str, Option<&str>); 3] = [
47
+ (
48
+ "CLIPS_DESKTOP_SENTRY_INGEST_HOST",
49
+ option_env!("CLIPS_DESKTOP_SENTRY_INGEST_HOST"),
50
+ ),
51
+ ("SENTRY_INGEST_HOST", option_env!("SENTRY_INGEST_HOST")),
52
+ (
53
+ "VITE_SENTRY_INGEST_HOST",
54
+ option_env!("VITE_SENTRY_INGEST_HOST"),
55
+ ),
56
+ ];
57
+
58
+ const ENVIRONMENT_ENV: [(&str, Option<&str>); 5] = [
59
+ (
60
+ "CLIPS_DESKTOP_SENTRY_ENVIRONMENT",
61
+ option_env!("CLIPS_DESKTOP_SENTRY_ENVIRONMENT"),
62
+ ),
63
+ ("SENTRY_ENVIRONMENT", option_env!("SENTRY_ENVIRONMENT")),
64
+ ("NETLIFY_CONTEXT", option_env!("NETLIFY_CONTEXT")),
65
+ ("VERCEL_ENV", option_env!("VERCEL_ENV")),
66
+ ("NODE_ENV", option_env!("NODE_ENV")),
67
+ ];
68
+
69
+ pub fn init() {
70
+ if SENTRY_GUARD.get().is_some() {
71
+ return;
72
+ }
73
+
74
+ let Some(dsn) = resolve_sentry_dsn() else {
75
+ return;
76
+ };
77
+ let Ok(dsn) = dsn.parse() else {
78
+ eprintln!("[clips-tray] ignoring invalid Sentry DSN");
79
+ return;
80
+ };
81
+
82
+ let guard = sentry::init(sentry::ClientOptions {
83
+ dsn: Some(dsn),
84
+ environment: Some(resolve_sentry_environment().into()),
85
+ release: Some(format!("clips-desktop@{}", env!("CARGO_PKG_VERSION")).into()),
86
+ send_default_pii: false,
87
+ before_send: Some(std::sync::Arc::new(|mut event| {
88
+ event.user = None;
89
+ Some(event)
90
+ })),
91
+ ..Default::default()
92
+ });
93
+
94
+ if guard.is_enabled() {
95
+ sentry::configure_scope(|scope| {
96
+ scope.set_tag("app", "agent-native-clips");
97
+ scope.set_tag("template", "clips");
98
+ scope.set_tag("runtime", "tauri-native");
99
+ });
100
+ }
101
+
102
+ let _ = SENTRY_GUARD.set(Box::leak(Box::new(guard)));
103
+ }
104
+
105
+ pub fn flush(timeout: Duration) {
106
+ if let Some(guard) = SENTRY_GUARD.get() {
107
+ guard.close(Some(timeout));
108
+ }
109
+ }
110
+
111
+ fn resolve_sentry_dsn() -> Option<String> {
112
+ if let Some(dsn) = first_env(&DIRECT_DSN_ENV) {
113
+ return Some(dsn);
114
+ }
115
+
116
+ let key = first_env(&KEY_ENV)?;
117
+ let project = first_env(&PROJECT_ENV)?;
118
+ let host = first_env(&HOST_ENV)?;
119
+ Some(format!("https://{key}@{host}/{project}"))
120
+ }
121
+
122
+ fn resolve_sentry_environment() -> String {
123
+ first_env(&ENVIRONMENT_ENV).unwrap_or_else(|| "production".to_string())
124
+ }
125
+
126
+ fn first_env(values: &[(&str, Option<&str>)]) -> Option<String> {
127
+ for (name, embedded) in values {
128
+ if let Ok(value) = std::env::var(name) {
129
+ let trimmed = value.trim();
130
+ if !trimmed.is_empty() {
131
+ return Some(trimmed.to_string());
132
+ }
133
+ }
134
+ if let Some(value) = embedded {
135
+ let trimmed = value.trim();
136
+ if !trimmed.is_empty() {
137
+ return Some(trimmed.to_string());
138
+ }
139
+ }
140
+ }
141
+ None
142
+ }
@@ -1,6 +1,65 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
1
5
  import react from "@vitejs/plugin-react";
2
6
  import { defineConfig } from "vite";
3
7
 
8
+ const root = dirname(fileURLToPath(import.meta.url));
9
+ const packageJson = JSON.parse(
10
+ readFileSync(resolve(root, "package.json"), "utf8"),
11
+ ) as { version?: string };
12
+
13
+ function firstNonEmpty(...values: Array<string | undefined>): string {
14
+ for (const value of values) {
15
+ const trimmed = value?.trim();
16
+ if (trimmed) return trimmed;
17
+ }
18
+ return "";
19
+ }
20
+
21
+ function resolveSentryDsn(): string {
22
+ const direct = firstNonEmpty(
23
+ process.env.CLIPS_DESKTOP_SENTRY_DSN,
24
+ process.env.TAURI_SENTRY_DSN,
25
+ process.env.SENTRY_DESKTOP_DSN,
26
+ process.env.SENTRY_CLIENT_DSN,
27
+ process.env.VITE_SENTRY_CLIENT_DSN,
28
+ process.env.VITE_SENTRY_DSN,
29
+ process.env.SENTRY_DSN,
30
+ );
31
+ if (direct) return direct;
32
+
33
+ const key = firstNonEmpty(
34
+ process.env.CLIPS_DESKTOP_SENTRY_CLIENT_KEY,
35
+ process.env.SENTRY_CLIENT_KEY,
36
+ process.env.VITE_SENTRY_CLIENT_KEY,
37
+ );
38
+ const projectId = firstNonEmpty(
39
+ process.env.CLIPS_DESKTOP_SENTRY_PROJECT_ID,
40
+ process.env.SENTRY_PROJECT_ID,
41
+ process.env.VITE_SENTRY_PROJECT_ID,
42
+ );
43
+ const host = firstNonEmpty(
44
+ process.env.CLIPS_DESKTOP_SENTRY_INGEST_HOST,
45
+ process.env.SENTRY_INGEST_HOST,
46
+ process.env.VITE_SENTRY_INGEST_HOST,
47
+ );
48
+ return key && projectId && host ? `https://${key}@${host}/${projectId}` : "";
49
+ }
50
+
51
+ function resolveSentryEnvironment(): string {
52
+ return (
53
+ firstNonEmpty(
54
+ process.env.CLIPS_DESKTOP_SENTRY_ENVIRONMENT,
55
+ process.env.SENTRY_ENVIRONMENT,
56
+ process.env.NETLIFY_CONTEXT,
57
+ process.env.VERCEL_ENV,
58
+ process.env.NODE_ENV,
59
+ ) || "production"
60
+ );
61
+ }
62
+
4
63
  // Tauri expects the frontend to be served from a fixed port during dev.
5
64
  // 1420 is the convention the Tauri docs use; we keep it here so
6
65
  // `tauri dev` and `vite dev` stay in sync out of the box.
@@ -20,6 +79,13 @@ import { defineConfig } from "vite";
20
79
  export default defineConfig({
21
80
  plugins: [react()],
22
81
  clearScreen: false,
82
+ define: {
83
+ __CLIPS_DESKTOP_SENTRY_DSN__: JSON.stringify(resolveSentryDsn()),
84
+ __CLIPS_DESKTOP_SENTRY_ENVIRONMENT__: JSON.stringify(
85
+ resolveSentryEnvironment(),
86
+ ),
87
+ __CLIPS_DESKTOP_VERSION__: JSON.stringify(packageJson.version ?? "0.0.0"),
88
+ },
23
89
  server: {
24
90
  port: 1420,
25
91
  strictPort: true,
@@ -2,4 +2,5 @@
2
2
  type: improved
3
3
  date: 2026-06-27
4
4
  ---
5
+
5
6
  Improved mobile navigation chrome and sidebar drawer motion.
@@ -6,6 +6,7 @@ import { eq, desc } from "drizzle-orm";
6
6
  import { z } from "zod";
7
7
 
8
8
  import { getDb, schema } from "../server/db/index.js";
9
+ import { publicSubmitterEmail } from "../shared/submitter-email.js";
9
10
 
10
11
  export default defineAction({
11
12
  description: "Export form responses to CSV or JSON file.",
@@ -36,7 +37,7 @@ export default defineAction({
36
37
  const data = responses.map((r) => ({
37
38
  id: r.id,
38
39
  submittedAt: r.submittedAt,
39
- submitterEmail: r.submitterEmail ?? null,
40
+ submitterEmail: publicSubmitterEmail(r.submitterEmail),
40
41
  pageUrl: r.pageUrl ?? null,
41
42
  clientSurface: r.clientSurface ?? null,
42
43
  ...JSON.parse(r.data),
@@ -56,7 +57,7 @@ export default defineAction({
56
57
  return [
57
58
  r.id,
58
59
  r.submittedAt,
59
- r.submitterEmail ?? "",
60
+ publicSubmitterEmail(r.submitterEmail) ?? "",
60
61
  r.pageUrl ?? "",
61
62
  r.clientSurface ?? "",
62
63
  ...fields.map((f: any) => {
@@ -4,6 +4,7 @@ import { eq, desc, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { publicSubmitterEmail } from "../shared/submitter-email.js";
7
8
  import type { FormResponse } from "../shared/types.js";
8
9
 
9
10
  export default defineAction({
@@ -49,7 +50,7 @@ export default defineAction({
49
50
  formId: r.formId,
50
51
  data: JSON.parse(r.data),
51
52
  submittedAt: r.submittedAt,
52
- submitterEmail: r.submitterEmail ?? null,
53
+ submitterEmail: publicSubmitterEmail(r.submitterEmail),
53
54
  pageUrl: r.pageUrl ?? null,
54
55
  clientSurface: r.clientSurface ?? null,
55
56
  })) as FormResponse[],
@@ -14,6 +14,7 @@ import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
14
14
  import { z } from "zod";
15
15
 
16
16
  import { getDb, schema } from "../server/db/index.js";
17
+ import { publicSubmitterEmail } from "../shared/submitter-email.js";
17
18
  import type {
18
19
  FormField,
19
20
  ResponseInsightsChartSeries,
@@ -161,7 +162,9 @@ function buildSpecificFormTable(
161
162
  totalRows: number,
162
163
  ): ResponseInsightsTable {
163
164
  const fields = safeJson<FormField[]>(form.fields, []).slice(0, 7);
164
- const hasSubmitter = responses.some((row) => row.submitterEmail);
165
+ const hasSubmitter = responses.some((row) =>
166
+ publicSubmitterEmail(row.submitterEmail),
167
+ );
165
168
  const columns = [
166
169
  { key: "submittedAt", label: "Submitted" },
167
170
  ...(hasSubmitter ? [{ key: "submitterEmail", label: "Email" }] : []),
@@ -173,7 +176,9 @@ function buildSpecificFormTable(
173
176
  id: response.id,
174
177
  submittedAt: response.submittedAt,
175
178
  ...(hasSubmitter
176
- ? { submitterEmail: response.submitterEmail ?? "" }
179
+ ? {
180
+ submitterEmail: publicSubmitterEmail(response.submitterEmail) ?? "",
181
+ }
177
182
  : {}),
178
183
  ...Object.fromEntries(
179
184
  fields.map((field) => [field.id, cleanText(data[field.id])]),
@@ -198,7 +203,9 @@ function buildAllFormsTable(
198
203
  tableLimit: number,
199
204
  totalRows: number,
200
205
  ): ResponseInsightsTable {
201
- const hasSubmitter = responses.some((row) => row.submitterEmail);
206
+ const hasSubmitter = responses.some((row) =>
207
+ publicSubmitterEmail(row.submitterEmail),
208
+ );
202
209
  const columns = [
203
210
  { key: "submittedAt", label: "Submitted" },
204
211
  { key: "form", label: "Form" },
@@ -210,7 +217,9 @@ function buildAllFormsTable(
210
217
  formId: response.formId,
211
218
  submittedAt: response.submittedAt,
212
219
  form: formsById.get(response.formId)?.title ?? response.formId,
213
- ...(hasSubmitter ? { submitterEmail: response.submitterEmail ?? "" } : {}),
220
+ ...(hasSubmitter
221
+ ? { submitterEmail: publicSubmitterEmail(response.submitterEmail) ?? "" }
222
+ : {}),
214
223
  summary: responseSummary(response, fieldsByForm),
215
224
  }));
216
225
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-27
4
+ ---
5
+
6
+ Anonymous feedback no longer shows internal Agent Native anonymous-owner emails as submitter addresses.
@@ -17,6 +17,10 @@ import {
17
17
  } from "h3";
18
18
  import { nanoid } from "nanoid";
19
19
 
20
+ import {
21
+ cleanSubmitterEmail,
22
+ publicSubmitterEmail,
23
+ } from "../../shared/submitter-email.js";
20
24
  import type {
21
25
  FormField,
22
26
  FormIntegration,
@@ -42,13 +46,6 @@ function cleanMetaText(value: unknown): string | null {
42
46
  return trimmed.slice(0, MAX_META_TEXT_LENGTH);
43
47
  }
44
48
 
45
- function cleanSubmitterEmail(value: unknown): string | null {
46
- if (typeof value !== "string") return null;
47
- const trimmed = value.trim();
48
- if (!trimmed || trimmed.length > 320 || !trimmed.includes("@")) return null;
49
- return trimmed;
50
- }
51
-
52
49
  // Allowlist the client-surface hint so only known values are stored. Anything
53
50
  // else (including spoofed direct POSTs) is dropped to NULL.
54
51
  const KNOWN_CLIENT_SURFACES = new Set(["web", "electron", "tauri"]);
@@ -340,7 +337,7 @@ export const listResponses = defineEventHandler(async (event: H3Event) => {
340
337
  formId: r.formId,
341
338
  data: JSON.parse(r.data),
342
339
  submittedAt: r.submittedAt,
343
- submitterEmail: r.submitterEmail,
340
+ submitterEmail: publicSubmitterEmail(r.submitterEmail),
344
341
  pageUrl: r.pageUrl ?? null,
345
342
  clientSurface: r.clientSurface ?? null,
346
343
  })) as FormResponse[],
@@ -3,6 +3,7 @@ import {
3
3
  ssrfSafeToolFetch,
4
4
  } from "@agent-native/core/tools/url-safety";
5
5
 
6
+ import { publicSubmitterEmail } from "../../shared/submitter-email.js";
6
7
  import type {
7
8
  FormIntegration,
8
9
  FormField,
@@ -155,6 +156,7 @@ function formatDebugContext(submission: SubmissionPayload): string[] {
155
156
 
156
157
  /** Slack Block Kit message */
157
158
  export function buildSlackPayload(submission: SubmissionPayload) {
159
+ const submitterEmail = publicSubmitterEmail(submission.submitterEmail);
158
160
  const fieldLines = submission.fields
159
161
  .filter((f) => submission.data[f.id] !== undefined)
160
162
  .map((f) => {
@@ -164,8 +166,8 @@ export function buildSlackPayload(submission: SubmissionPayload) {
164
166
  });
165
167
 
166
168
  const tsContext = `Submitted <!date^${Math.floor(new Date(submission.submittedAt).getTime() / 1000)}^{date_short_pretty} at {time}|${submission.submittedAt}>`;
167
- const contextText = submission.submitterEmail
168
- ? `${tsContext} by *${submission.submitterEmail}*`
169
+ const contextText = submitterEmail
170
+ ? `${tsContext} by *${submitterEmail}*`
169
171
  : tsContext;
170
172
  const debugContext = formatDebugContext(submission);
171
173
 
@@ -201,6 +203,7 @@ export function buildSlackPayload(submission: SubmissionPayload) {
201
203
 
202
204
  /** Discord webhook embed */
203
205
  function buildDiscordPayload(submission: SubmissionPayload) {
206
+ const submitterEmail = publicSubmitterEmail(submission.submitterEmail);
204
207
  const discordFields = submission.fields
205
208
  .filter((f) => submission.data[f.id] !== undefined)
206
209
  .map((f) => {
@@ -208,10 +211,10 @@ function buildDiscordPayload(submission: SubmissionPayload) {
208
211
  const display = Array.isArray(val) ? val.join(", ") : String(val);
209
212
  return { name: f.label, value: display, inline: true };
210
213
  });
211
- if (submission.submitterEmail) {
214
+ if (submitterEmail) {
212
215
  discordFields.push({
213
216
  name: "Submitted by",
214
- value: submission.submitterEmail,
217
+ value: submitterEmail,
215
218
  inline: true,
216
219
  });
217
220
  }
@@ -261,7 +264,7 @@ function buildGoogleSheetsPayload(submission: SubmissionPayload) {
261
264
  return {
262
265
  formTitle: submission.formTitle,
263
266
  submittedAt: submission.submittedAt,
264
- submitterEmail: submission.submitterEmail ?? "",
267
+ submitterEmail: publicSubmitterEmail(submission.submitterEmail) ?? "",
265
268
  chatSessionIds: (submission.chatSessionIds ?? []).join(", "),
266
269
  activeRunId: submission.activeRunId ?? "",
267
270
  pageUrl: submission.pageUrl ?? "",
@@ -278,7 +281,7 @@ function buildWebhookPayload(submission: SubmissionPayload) {
278
281
  formTitle: submission.formTitle,
279
282
  responseId: submission.responseId,
280
283
  submittedAt: submission.submittedAt,
281
- submitterEmail: submission.submitterEmail ?? null,
284
+ submitterEmail: publicSubmitterEmail(submission.submitterEmail),
282
285
  chatSessionIds: submission.chatSessionIds ?? [],
283
286
  activeRunId: submission.activeRunId ?? null,
284
287
  pageUrl: submission.pageUrl ?? null,