drafted 1.7.18 → 1.7.20
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/install-mcp.sh +45 -1
- package/mcp/server.mjs +112 -7
- package/package.json +4 -2
- package/server/lib/umami.mjs +156 -0
package/install-mcp.sh
CHANGED
|
@@ -215,8 +215,10 @@ export PATH="$HOME/.drafted/npm-global/bin:$PATH"
|
|
|
215
215
|
|
|
216
216
|
step "Installing Drafted"
|
|
217
217
|
|
|
218
|
-
npm install -g drafted@latest --force
|
|
218
|
+
npm install -g drafted@latest --force
|
|
219
219
|
hash -r 2>/dev/null || true
|
|
220
|
+
NPM_ROOT="$(npm root -g)"
|
|
221
|
+
node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$NPM_ROOT/drafted/mcp/server.mjs"
|
|
220
222
|
ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
|
|
221
223
|
|
|
222
224
|
# ── Configure ─────────────────────────────────────────────────────
|
|
@@ -586,6 +588,7 @@ struct DraftedUpdaterApp: App {
|
|
|
586
588
|
var body: some Scene {
|
|
587
589
|
MenuBarExtra {
|
|
588
590
|
Button("Update Drafted") { Actions.updateDrafted() }
|
|
591
|
+
Button("View Logs") { Actions.viewLogs() }
|
|
589
592
|
Button("Open Drafted") { Actions.openDrafted() }
|
|
590
593
|
Divider()
|
|
591
594
|
Button("Quit") { NSApp.terminate(nil) }
|
|
@@ -735,6 +738,47 @@ struct Actions {
|
|
|
735
738
|
}
|
|
736
739
|
}
|
|
737
740
|
|
|
741
|
+
static func viewLogs() {
|
|
742
|
+
let fm = FileManager.default
|
|
743
|
+
let home = fm.homeDirectoryForCurrentUser
|
|
744
|
+
let fallback = home.appendingPathComponent(".drafted")
|
|
745
|
+
try? fm.createDirectory(at: fallback, withIntermediateDirectories: true)
|
|
746
|
+
let readme = fallback.appendingPathComponent("logs-readme.txt")
|
|
747
|
+
if !fm.fileExists(atPath: readme.path) {
|
|
748
|
+
let text = """
|
|
749
|
+
Drafted MCP logs are written by the host app that launched drafted-mcp.
|
|
750
|
+
|
|
751
|
+
Most useful locations:
|
|
752
|
+
- Drafted MCP client errors: ~/.drafted/mcp-client.log
|
|
753
|
+
- Claude Desktop: ~/Library/Logs/Claude/mcp-server-drafted.log
|
|
754
|
+
- Claude Code: ~/.claude/projects (session transcripts) or ~/.claude/logs when present
|
|
755
|
+
- Drafted installer/updater: ~/.drafted
|
|
756
|
+
|
|
757
|
+
If a tool returns `fetch failed`, open the Claude log or transcript from the same run and look for `mcp-server-drafted` or `Drafted MCP`.
|
|
758
|
+
"""
|
|
759
|
+
try? text.write(to: readme, atomically: true, encoding: .utf8)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let candidates = [
|
|
763
|
+
home.appendingPathComponent(".drafted/mcp-client.log"),
|
|
764
|
+
home.appendingPathComponent("Library/Logs/Claude/mcp-server-drafted.log"),
|
|
765
|
+
home.appendingPathComponent("Library/Logs/Claude"),
|
|
766
|
+
home.appendingPathComponent(".claude/logs"),
|
|
767
|
+
home.appendingPathComponent(".claude/projects"),
|
|
768
|
+
fallback,
|
|
769
|
+
]
|
|
770
|
+
for url in candidates where fm.fileExists(atPath: url.path) {
|
|
771
|
+
var isDirectory: ObjCBool = false
|
|
772
|
+
fm.fileExists(atPath: url.path, isDirectory: &isDirectory)
|
|
773
|
+
if isDirectory.boolValue {
|
|
774
|
+
NSWorkspace.shared.open(url)
|
|
775
|
+
} else {
|
|
776
|
+
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
777
|
+
}
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
738
782
|
static func openDrafted() {
|
|
739
783
|
NSWorkspace.shared.open(URL(string: "https://drafted.live")!)
|
|
740
784
|
}
|
package/mcp/server.mjs
CHANGED
|
@@ -10,9 +10,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
|
-
import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
13
|
+
import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync, unlinkSync, appendFileSync } from 'fs';
|
|
14
14
|
import { join, dirname, basename, extname, resolve } from 'path';
|
|
15
|
-
import { homedir } from 'os';
|
|
15
|
+
import { homedir, platform, release as osRelease, arch as osArch } from 'os';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
18
18
|
import { z } from 'zod';
|
|
@@ -20,7 +20,21 @@ import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/e
|
|
|
20
20
|
import WebSocket from 'ws';
|
|
21
21
|
import { LAYERS } from '../src/shared/constants.mjs';
|
|
22
22
|
import { emptyExcalidrawScene, excalidrawSceneFromMermaid, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
|
|
23
|
-
|
|
23
|
+
const { UMAMI_EVENTS, trackUmamiEvent } = await (async () => {
|
|
24
|
+
try {
|
|
25
|
+
return await import('../server/lib/umami.mjs');
|
|
26
|
+
} catch {
|
|
27
|
+
return {
|
|
28
|
+
UMAMI_EVENTS: Object.freeze({
|
|
29
|
+
MCP_CONNECTED: 'mcp_connected',
|
|
30
|
+
MCP_TOOL_CALLED: 'mcp_tool_called',
|
|
31
|
+
DRAFTED_MCP_REQUEST: 'drafted_mcp_request',
|
|
32
|
+
DRAFTED_MCP_ERROR: 'drafted_mcp_error',
|
|
33
|
+
}),
|
|
34
|
+
trackUmamiEvent: () => {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
24
38
|
|
|
25
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
40
|
|
|
@@ -202,9 +216,15 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
|
|
|
202
216
|
}
|
|
203
217
|
return server.registerTool(name, config, async (...args) => {
|
|
204
218
|
const state = getState();
|
|
219
|
+
const previousTool = state.currentTool;
|
|
220
|
+
state.currentTool = name;
|
|
205
221
|
trackUmamiEvent(UMAMI_EVENTS.MCP_TOOL_CALLED, { tool: name, projectId: state.projectId || undefined, source: 'mcp' });
|
|
206
|
-
reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST);
|
|
207
|
-
|
|
222
|
+
reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST, { tool: name });
|
|
223
|
+
try {
|
|
224
|
+
return await cb(...args);
|
|
225
|
+
} finally {
|
|
226
|
+
state.currentTool = previousTool;
|
|
227
|
+
}
|
|
208
228
|
});
|
|
209
229
|
}
|
|
210
230
|
|
|
@@ -301,7 +321,25 @@ function getInstallInfo() {
|
|
|
301
321
|
}
|
|
302
322
|
}
|
|
303
323
|
|
|
304
|
-
|
|
324
|
+
|
|
325
|
+
function osFamily() {
|
|
326
|
+
const p = platform();
|
|
327
|
+
if (p === 'darwin') return 'macos';
|
|
328
|
+
if (p === 'win32') return 'windows';
|
|
329
|
+
if (p === 'linux') return 'linux';
|
|
330
|
+
return 'unknown';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizedArch() {
|
|
334
|
+
const a = osArch();
|
|
335
|
+
return ['x64', 'arm64', 'arm', 'ia32'].includes(a) ? a : 'unknown';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function mcpMode() {
|
|
339
|
+
return process.argv.includes('--http') ? 'http' : 'stdio';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function reportInstallationEvent(event, extra = {}) {
|
|
305
343
|
const info = getInstallInfo();
|
|
306
344
|
if (!info) return;
|
|
307
345
|
fetch(`${getServerUrl()}/api/installations/report`, {
|
|
@@ -312,12 +350,78 @@ function reportInstallationEvent(event) {
|
|
|
312
350
|
event,
|
|
313
351
|
schemaVersion: 1,
|
|
314
352
|
cliVersion: PACKAGE_VERSION,
|
|
315
|
-
|
|
353
|
+
osFamily: osFamily(),
|
|
354
|
+
osVersion: osRelease().slice(0, 60),
|
|
355
|
+
arch: normalizedArch(),
|
|
356
|
+
nodeVersion: process.version,
|
|
357
|
+
mcpMode: mcpMode(),
|
|
316
358
|
source: 'mcp',
|
|
359
|
+
...extra,
|
|
317
360
|
}),
|
|
318
361
|
}).catch(() => {});
|
|
319
362
|
}
|
|
320
363
|
|
|
364
|
+
function classifyMcpError(error) {
|
|
365
|
+
const msg = String(error?.message || error || '').toLowerCase();
|
|
366
|
+
const causeCode = String(error?.cause?.code || error?.code || '').toLowerCase();
|
|
367
|
+
if (causeCode) return causeCode.slice(0, 80);
|
|
368
|
+
if (msg.includes('fetch failed')) return 'fetch_failed';
|
|
369
|
+
if (msg.includes('failed to fetch')) return 'fetch_failed';
|
|
370
|
+
if (msg.includes('network')) return 'network_error';
|
|
371
|
+
if (msg.includes('timeout') || msg.includes('timed out')) return 'timeout';
|
|
372
|
+
const httpMatch = msg.match(/http\s+(\d{3})/);
|
|
373
|
+
if (httpMatch) return `http_${httpMatch[1]}`;
|
|
374
|
+
if (msg.includes('unauthorized') || msg.includes('401')) return 'auth_401';
|
|
375
|
+
if (msg.includes('forbidden') || msg.includes('403')) return 'auth_403';
|
|
376
|
+
return 'tool_error';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function scrubLogValue(value) {
|
|
380
|
+
if (typeof value !== 'string') return value;
|
|
381
|
+
return value
|
|
382
|
+
.replace(/([?&](?:token|code|dci|session|auth|password|secret)=)[^&\s]+/gi, '$1[Filtered]')
|
|
383
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi, '$1[Filtered]')
|
|
384
|
+
.replace(/(gc_session=)[^;\s]+/gi, '$1[Filtered]')
|
|
385
|
+
.slice(0, 500);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function serverOriginForLog() {
|
|
389
|
+
try { return new URL(getServerUrl()).origin; } catch { return 'invalid_server_url'; }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function writeMcpClientLog(tool, error) {
|
|
393
|
+
const entry = {
|
|
394
|
+
ts: new Date().toISOString(),
|
|
395
|
+
level: 'error',
|
|
396
|
+
component: 'drafted-mcp-client',
|
|
397
|
+
version: PACKAGE_VERSION,
|
|
398
|
+
tool: String(tool || 'unknown').slice(0, 80),
|
|
399
|
+
errorCode: classifyMcpError(error),
|
|
400
|
+
message: scrubLogValue(error?.message || String(error)),
|
|
401
|
+
causeCode: scrubLogValue(error?.cause?.code || error?.code || ''),
|
|
402
|
+
causeMessage: scrubLogValue(error?.cause?.message || ''),
|
|
403
|
+
server: serverOriginForLog(),
|
|
404
|
+
osFamily: osFamily(),
|
|
405
|
+
nodeVersion: process.version,
|
|
406
|
+
mcpMode: mcpMode(),
|
|
407
|
+
};
|
|
408
|
+
const line = `[Drafted MCP] ${JSON.stringify(entry)}\n`;
|
|
409
|
+
try { console.error(line.trimEnd()); } catch { /* ignore */ }
|
|
410
|
+
try {
|
|
411
|
+
const logPath = join(homedir(), '.drafted', 'mcp-client.log');
|
|
412
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
413
|
+
appendFileSync(logPath, line, { mode: 0o600 });
|
|
414
|
+
} catch { /* best effort */ }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function reportMcpToolError(tool, error) {
|
|
418
|
+
writeMcpClientLog(tool, error);
|
|
419
|
+
reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_ERROR, {
|
|
420
|
+
tool: String(tool || 'unknown').slice(0, 80),
|
|
421
|
+
errorCode: classifyMcpError(error),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
321
425
|
function wikiBrowserUrl(path = '') {
|
|
322
426
|
const normalized = normalizeWikiPath(path || '');
|
|
323
427
|
if (!normalized) return `${getServerUrl()}/wiki`;
|
|
@@ -702,6 +806,7 @@ if (!globalThis.__draftedAgentWsBootstrapped) {
|
|
|
702
806
|
}
|
|
703
807
|
|
|
704
808
|
function err(error) {
|
|
809
|
+
reportMcpToolError(getState().currentTool, error);
|
|
705
810
|
return { content: [{ type: 'text', text: error.message || String(error) }], isError: true };
|
|
706
811
|
}
|
|
707
812
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.20",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"cli/",
|
|
8
8
|
"mcp/",
|
|
9
|
+
"server/lib/umami.mjs",
|
|
9
10
|
"src/shared/",
|
|
10
11
|
"agent-instructions/",
|
|
11
12
|
"install-mcp.sh"
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
"version:check": "node scripts/sync-versions.mjs",
|
|
32
33
|
"postpublish": "bash scripts/sync-plugin.sh \"chore: sync plugin to v$npm_package_version\"",
|
|
33
34
|
"deploy:check:google": "node scripts/check-google-drive-deploy.mjs",
|
|
34
|
-
"build:excalidraw": "node scripts/build-excalidraw-editor.mjs"
|
|
35
|
+
"build:excalidraw": "node scripts/build-excalidraw-editor.mjs",
|
|
36
|
+
"prepublishOnly": "node scripts/check-npm-package.mjs"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
39
|
"@aws-sdk/client-s3": "^3.1007.0",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const UMAMI_EVENTS = Object.freeze({
|
|
2
|
+
SIGNUP_STARTED: 'signup_started',
|
|
3
|
+
SIGNUP_COMPLETED: 'signup_completed',
|
|
4
|
+
ORG_CREATED: 'org_created',
|
|
5
|
+
PROJECT_CREATED: 'project_created',
|
|
6
|
+
PROJECT_OPENED: 'project_opened',
|
|
7
|
+
TEMPLATE_USED: 'template_used',
|
|
8
|
+
FRAME_CREATED: 'frame_created',
|
|
9
|
+
FRAME_UPDATED: 'frame_updated',
|
|
10
|
+
FRAME_DELETED: 'frame_deleted',
|
|
11
|
+
SHARE_CREATED: 'share_created',
|
|
12
|
+
SHARE_ACCEPTED: 'share_accepted',
|
|
13
|
+
INVITE_SENT: 'invite_sent',
|
|
14
|
+
MCP_CONNECTED: 'mcp_connected',
|
|
15
|
+
MCP_TOOL_CALLED: 'mcp_tool_called',
|
|
16
|
+
WIKI_PAGE_CREATED: 'wiki_page_created',
|
|
17
|
+
SKILL_ATTACHED: 'skill_attached',
|
|
18
|
+
DRIVE_CONNECTED: 'drive_connected',
|
|
19
|
+
DRIVE_SYNC_COMPLETED: 'drive_sync_completed',
|
|
20
|
+
DRAFTED_INSTALL: 'drafted_install',
|
|
21
|
+
DRAFTED_UPDATE: 'drafted_update',
|
|
22
|
+
DRAFTED_HEARTBEAT: 'drafted_heartbeat',
|
|
23
|
+
DRAFTED_MCP_CONFIGURED: 'drafted_mcp_configured',
|
|
24
|
+
DRAFTED_MCP_REQUEST: 'drafted_mcp_request',
|
|
25
|
+
DRAFTED_MCP_ERROR: 'drafted_mcp_error',
|
|
26
|
+
DRAFTED_UPDATE_HELPER_STARTED: 'drafted_update_helper_started',
|
|
27
|
+
DRAFTED_UPDATE_HELPER_FAILED: 'drafted_update_helper_failed',
|
|
28
|
+
CTA_CLICKED: 'cta_clicked',
|
|
29
|
+
TEMPLATE_SELECTED: 'template_selected',
|
|
30
|
+
PROJECT_NAVIGATED: 'project_navigated',
|
|
31
|
+
SHARE_MODAL_OPENED: 'share_modal_opened',
|
|
32
|
+
SHARE_MODAL_ACTION: 'share_modal_action',
|
|
33
|
+
SURFACE_LOADED: 'surface_loaded',
|
|
34
|
+
SIDEBAR_USED: 'sidebar_used',
|
|
35
|
+
TOOL_PANEL_USED: 'tool_panel_used',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const BLOCKED_KEYS = new Set([
|
|
39
|
+
'email', 'userEmail', 'name', 'userName', 'orgName', 'projectName', 'label', 'title',
|
|
40
|
+
'content', 'html', 'markdown', 'body', 'prompt', 'token', 'cookie', 'authorization',
|
|
41
|
+
'password', 'secret', 'apiKey', 'request', 'headers', 'file', 'buffer', 'dataUrl',
|
|
42
|
+
]);
|
|
43
|
+
const ID_KEYS = new Set(['userId', 'orgId', 'projectId', 'projectSlug', 'frameId', 'shareId', 'skillId', 'templateId', 'templateSlug', 'userRole', 'role', 'buildId', 'pageType', 'tool', 'action', 'source', 'layer', 'lane', 'mode', 'installId', 'schemaVersion', 'installerVersion', 'cliVersion', 'osFamily', 'osVersion', 'arch', 'nodeVersion', 'npmVersion', 'claudeDesktop', 'claudeCode', 'codex', 'cursor', 'updateHelperStatus', 'mcpMode', 'errorCode']);
|
|
44
|
+
|
|
45
|
+
export function getUmamiConfig(config = {}) {
|
|
46
|
+
const hostUrl = (config.umamiHostUrl || process.env.UMAMI_HOST_URL || '').replace(/\/$/, '');
|
|
47
|
+
const websiteId = config.umamiWebsiteId || process.env.UMAMI_WEBSITE_ID || '';
|
|
48
|
+
return { hostUrl, websiteId, enabled: Boolean(hostUrl && websiteId) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sanitizeUmamiEventData(data = {}) {
|
|
52
|
+
const output = {};
|
|
53
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return output;
|
|
54
|
+
for (const [key, value] of Object.entries(data)) {
|
|
55
|
+
if (BLOCKED_KEYS.has(key)) continue;
|
|
56
|
+
if (value === undefined || value === null) continue;
|
|
57
|
+
if (typeof value === 'object') continue;
|
|
58
|
+
if (!ID_KEYS.has(key) && /email|name|content|html|markdown|body|prompt|token|cookie|auth|secret|password|key|file/i.test(key)) continue;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
if (value.length > 120) continue;
|
|
61
|
+
if (/@/.test(value)) continue;
|
|
62
|
+
output[key] = value;
|
|
63
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
64
|
+
output[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getUmamiHead(config = {}) {
|
|
71
|
+
const umami = getUmamiConfig(config);
|
|
72
|
+
if (!umami.enabled) return '';
|
|
73
|
+
const ctx = sanitizeUmamiEventData({
|
|
74
|
+
userId: config.userId,
|
|
75
|
+
orgId: config.orgId,
|
|
76
|
+
projectId: config.projectId,
|
|
77
|
+
projectSlug: config.projectSlug,
|
|
78
|
+
userRole: config.userRole,
|
|
79
|
+
buildId: config.buildId,
|
|
80
|
+
pageType: config.pageType,
|
|
81
|
+
});
|
|
82
|
+
return `<script defer src="${umami.hostUrl}/script.js" data-website-id="${umami.websiteId}"></script>
|
|
83
|
+
<script>
|
|
84
|
+
window.__DRAFTED_ANALYTICS__ = ${JSON.stringify(ctx)};
|
|
85
|
+
window.draftedTrack = function(name, data) {
|
|
86
|
+
var base = window.__DRAFTED_ANALYTICS__ || {};
|
|
87
|
+
var payload = Object.assign({}, base, data || {});
|
|
88
|
+
if (window.umami && typeof window.umami.track === 'function') window.umami.track(name, payload);
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener('load', function() {
|
|
91
|
+
var c = window.__DRAFTED_ANALYTICS__ || {};
|
|
92
|
+
if (c.userId && window.umami && typeof window.umami.identify === 'function') window.umami.identify(c.userId, { orgId: c.orgId, role: c.userRole });
|
|
93
|
+
});
|
|
94
|
+
</script>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getUmamiInteractionScript(pageType) {
|
|
98
|
+
return `<script>
|
|
99
|
+
(function() {
|
|
100
|
+
function track(name, data) { if (window.draftedTrack) window.draftedTrack(name, Object.assign({ pageType: ${JSON.stringify(pageType)} }, data || {})); }
|
|
101
|
+
window.addEventListener('load', function() {
|
|
102
|
+
if (${JSON.stringify(pageType)} === 'surface') track('surface_loaded');
|
|
103
|
+
});
|
|
104
|
+
document.addEventListener('click', function(event) {
|
|
105
|
+
var el = event.target && event.target.closest && event.target.closest('a,button,[data-analytics-event],[data-template-slug],[data-project-id]');
|
|
106
|
+
if (!el) return;
|
|
107
|
+
var eventName = el.getAttribute('data-analytics-event');
|
|
108
|
+
if (eventName) return track(eventName, { action: el.getAttribute('data-analytics-action') || undefined });
|
|
109
|
+
var href = el.getAttribute('href') || '';
|
|
110
|
+
var cls = el.className || '';
|
|
111
|
+
if (el.matches('.nav-cta,.btn-primary,.btn-secondary')) track('cta_clicked', { action: href || (el.textContent || '').trim().slice(0, 40) });
|
|
112
|
+
if (el.getAttribute('data-template-slug')) track('template_selected', { templateSlug: el.getAttribute('data-template-slug') });
|
|
113
|
+
if (el.getAttribute('data-project-id')) track('project_navigated', { projectId: el.getAttribute('data-project-id') });
|
|
114
|
+
if (/share/i.test(String(cls)) || /share/i.test(el.textContent || '')) track('share_modal_action');
|
|
115
|
+
if (/sidebar/i.test(String(cls))) track('sidebar_used');
|
|
116
|
+
if (/tool|panel/i.test(String(cls))) track('tool_panel_used');
|
|
117
|
+
}, true);
|
|
118
|
+
})();
|
|
119
|
+
</script>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getRequestUrl(req) {
|
|
123
|
+
if (!req) return undefined;
|
|
124
|
+
const host = typeof req.get === 'function' ? req.get('host') : req.headers?.host;
|
|
125
|
+
const protocol = req.protocol || 'https';
|
|
126
|
+
return host ? `${protocol}://${host}${req.originalUrl || req.url || ''}` : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function trackUmamiEvent(name, data = {}, req = null) {
|
|
130
|
+
const umami = getUmamiConfig();
|
|
131
|
+
if (!umami.enabled || !name || name.length > 50) return;
|
|
132
|
+
const payload = {
|
|
133
|
+
type: 'event',
|
|
134
|
+
payload: {
|
|
135
|
+
website: umami.websiteId,
|
|
136
|
+
name,
|
|
137
|
+
url: getRequestUrl(req) || process.env.BASE_URL || 'https://drafted.live',
|
|
138
|
+
hostname: req?.hostname || 'drafted.live',
|
|
139
|
+
language: req?.headers?.['accept-language'],
|
|
140
|
+
referrer: req?.headers?.referer,
|
|
141
|
+
data: sanitizeUmamiEventData(data),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
146
|
+
fetch(`${umami.hostUrl}/api/send`, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
'User-Agent': req?.headers?.['user-agent'] || 'Drafted Server Analytics',
|
|
151
|
+
'X-Forwarded-For': req?.ip || req?.headers?.['x-forwarded-for'] || '',
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify(payload),
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
}).catch(() => {}).finally(() => clearTimeout(timeout));
|
|
156
|
+
}
|