@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/docs/content/deployment.md +82 -82
- package/corpus/core/docs/content/faq.md +1 -1
- package/corpus/core/docs/content/tracking.md +3 -3
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/openai-compatible-endpoint.ts +25 -0
- package/corpus/core/src/agent/engine/provider-env-vars.ts +2 -0
- package/corpus/core/src/agent/engine/registry.ts +59 -11
- package/corpus/core/src/client/FeedbackButton.tsx +11 -1
- package/corpus/core/src/client/agent-engine-key.ts +57 -10
- package/corpus/core/src/client/analytics.ts +78 -27
- package/corpus/core/src/client/blocks/library/HighlightedCode.tsx +1 -1
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/session-replay.ts +159 -17
- package/corpus/core/src/client/settings/SettingsPanel.tsx +119 -4
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -0
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +8 -3
- package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +54 -6
- package/corpus/core/src/server/agent-engine-api-key-route.ts +83 -12
- package/corpus/core/src/server/core-routes-plugin.ts +26 -2
- package/corpus/core/src/styles/blocks.css +23 -7
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +22 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +64 -50
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +38 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +89 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +138 -59
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-charts-drag-smoothly-when-rearranging-panels.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-dashboard-email-reports-still-send-when-the-live-screenshot-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-fixed-dashboard-edits-so-charts-no-longer-briefly-snap-back-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/analytics/changelog/2026-06-27-session-replay-lists-now-show-only-signed-in-recordings-with.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-27-sidebar-loading-placeholders-are-visible-in-light-mode.md +6 -0
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/seed-session-replay.ts +1 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +120 -29
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +158 -46
- package/corpus/templates/assets/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/clips/app/components/capture-install-options.tsx +0 -9
- package/corpus/templates/clips/changelog/2026-06-27-clips-no-longer-shows-desktop-app-prompts-on-mobile-screens.md +1 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-download-options-now-show-the-chrome-extension-and-.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-recorder-setup-mobile-capture-options.md +1 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/sentry.ts +17 -1
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/check-tauri-prereqs.mjs +45 -0
- package/corpus/templates/clips/desktop/src/lib/sentry.ts +71 -0
- package/corpus/templates/clips/desktop/src/main.tsx +2 -0
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -1
- package/corpus/templates/clips/desktop/src-tauri/build.rs +29 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/sentry_report.rs +142 -0
- package/corpus/templates/clips/desktop/vite.config.ts +66 -0
- package/corpus/templates/design/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/corpus/templates/forms/actions/export-responses.ts +3 -2
- package/corpus/templates/forms/actions/list-responses.ts +2 -1
- package/corpus/templates/forms/actions/response-insights.ts +13 -4
- package/corpus/templates/forms/changelog/2026-06-27-anonymous-feedback-no-longer-shows-internal-agent-native-ano.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +5 -8
- package/corpus/templates/forms/server/lib/integrations.ts +9 -6
- package/corpus/templates/forms/shared/submitter-email.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +1 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +49 -10
- package/corpus/templates/slides/changelog/2026-06-27-mobile-navigation-chrome-and-motion.md +1 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +3 -0
- package/dist/agent/engine/openai-compatible-endpoint.d.ts.map +1 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +23 -0
- package/dist/agent/engine/openai-compatible-endpoint.js.map +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts +1 -0
- package/dist/agent/engine/provider-env-vars.d.ts.map +1 -1
- package/dist/agent/engine/provider-env-vars.js +1 -0
- package/dist/agent/engine/provider-env-vars.js.map +1 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +39 -12
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/client/FeedbackButton.d.ts.map +1 -1
- package/dist/client/FeedbackButton.js +6 -1
- package/dist/client/FeedbackButton.js.map +1 -1
- package/dist/client/agent-engine-key.d.ts +14 -0
- package/dist/client/agent-engine-key.d.ts.map +1 -1
- package/dist/client/agent-engine-key.js +35 -10
- package/dist/client/agent-engine-key.js.map +1 -1
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +71 -24
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
- package/dist/client/blocks/library/HighlightedCode.js +1 -1
- package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/session-replay.d.ts +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +115 -17
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +42 -6
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +4 -0
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +5 -4
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +37 -5
- package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +4 -4
- package/dist/server/agent-engine-api-key-route.d.ts.map +1 -1
- package/dist/server/agent-engine-api-key-route.js +71 -11
- package/dist/server/agent-engine-api-key-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/styles/blocks.css +23 -7
- package/docs/content/deployment.md +82 -82
- package/docs/content/faq.md +1 -1
- package/docs/content/tracking.md +3 -3
- package/package.json +1 -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.
|
|
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
|
|
@@ -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,
|
|
@@ -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
|
|
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
|
|
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) =>
|
|
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
|
-
? {
|
|
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) =>
|
|
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
|
|
220
|
+
...(hasSubmitter
|
|
221
|
+
? { submitterEmail: publicSubmitterEmail(response.submitterEmail) ?? "" }
|
|
222
|
+
: {}),
|
|
214
223
|
summary: responseSummary(response, fieldsByForm),
|
|
215
224
|
}));
|
|
216
225
|
|
|
@@ -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 =
|
|
168
|
-
? `${tsContext} by *${
|
|
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 (
|
|
214
|
+
if (submitterEmail) {
|
|
212
215
|
discordFields.push({
|
|
213
216
|
name: "Submitted by",
|
|
214
|
-
value:
|
|
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
|
|
284
|
+
submitterEmail: publicSubmitterEmail(submission.submitterEmail),
|
|
282
285
|
chatSessionIds: submission.chatSessionIds ?? [],
|
|
283
286
|
activeRunId: submission.activeRunId ?? null,
|
|
284
287
|
pageUrl: submission.pageUrl ?? null,
|