@watchforge/browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION_GUIDE.md +755 -0
- package/LICENSE +201 -0
- package/README.md +142 -0
- package/package.json +58 -0
- package/src/client.js +628 -0
- package/src/express.js +77 -0
- package/src/index.js +12 -0
- package/src/react.js +46 -0
- package/src/tracing.js +253 -0
- package/src/transport.js +191 -0
package/src/react.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React integration for WatchForge SDK
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { register } from '@watchforge/browser';
|
|
6
|
+
* import { ErrorBoundary } from '@watchforge/browser/react';
|
|
7
|
+
*
|
|
8
|
+
* register({ dsn: "...", app_env: "production" });
|
|
9
|
+
*
|
|
10
|
+
* <ErrorBoundary>
|
|
11
|
+
* <App />
|
|
12
|
+
* </ErrorBoundary>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { captureException } from './client.js';
|
|
17
|
+
|
|
18
|
+
export class ErrorBoundary extends React.Component {
|
|
19
|
+
constructor(props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = { hasError: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDerivedStateFromError(error) {
|
|
25
|
+
return { hasError: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidCatch(error, errorInfo) {
|
|
29
|
+
captureException(error, {
|
|
30
|
+
extra: {
|
|
31
|
+
componentStack: errorInfo.componentStack,
|
|
32
|
+
},
|
|
33
|
+
tags: {
|
|
34
|
+
framework: 'react',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
if (this.state.hasError) {
|
|
41
|
+
return this.props.fallback || React.createElement('h1', null, 'Something went wrong.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this.props.children;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/tracing.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Trace and Span instrumentation for WatchForge JavaScript SDK
|
|
2
|
+
|
|
3
|
+
let DSN = null;
|
|
4
|
+
let APP_ENV = "production";
|
|
5
|
+
let DEBUG = false;
|
|
6
|
+
|
|
7
|
+
// Active transaction stack (for nested transactions)
|
|
8
|
+
const transactionStack = [];
|
|
9
|
+
|
|
10
|
+
export function initTracing(dsn, environment = "production", debug = false) {
|
|
11
|
+
DSN = dsn;
|
|
12
|
+
APP_ENV = environment;
|
|
13
|
+
DEBUG = debug;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Span {
|
|
17
|
+
constructor(spanId, op, description = "", parentSpanId = null, data = {}) {
|
|
18
|
+
this.span_id = spanId;
|
|
19
|
+
this.op = op;
|
|
20
|
+
this.description = description;
|
|
21
|
+
this.parent_span_id = parentSpanId;
|
|
22
|
+
this.data = data;
|
|
23
|
+
this.start_timestamp = Date.now();
|
|
24
|
+
this.finish_timestamp = null;
|
|
25
|
+
this.duration_ms = 0;
|
|
26
|
+
this.status = "ok";
|
|
27
|
+
this.status_code = null;
|
|
28
|
+
this.tags = {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
finish(status = "ok", statusCode = null) {
|
|
32
|
+
this.finish_timestamp = Date.now();
|
|
33
|
+
this.duration_ms = this.finish_timestamp - this.start_timestamp;
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.status_code = statusCode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setTag(key, value) {
|
|
39
|
+
this.tags[key] = value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setData(key, value) {
|
|
43
|
+
this.data[key] = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toJSON() {
|
|
47
|
+
return {
|
|
48
|
+
span_id: this.span_id,
|
|
49
|
+
parent_span_id: this.parent_span_id,
|
|
50
|
+
op: this.op,
|
|
51
|
+
description: this.description,
|
|
52
|
+
start_timestamp: new Date(this.start_timestamp).toISOString(),
|
|
53
|
+
finish_timestamp: this.finish_timestamp ? new Date(this.finish_timestamp).toISOString() : null,
|
|
54
|
+
duration_ms: this.duration_ms,
|
|
55
|
+
status: this.status,
|
|
56
|
+
status_code: this.status_code,
|
|
57
|
+
data: this.data,
|
|
58
|
+
tags: this.tags,
|
|
59
|
+
timestamp: new Date(this.start_timestamp).toISOString(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Transaction {
|
|
65
|
+
constructor(traceId, transaction, transactionName = "", op = "http.server") {
|
|
66
|
+
this.trace_id = traceId;
|
|
67
|
+
this.transaction = transaction;
|
|
68
|
+
this.transaction_name = transactionName || transaction;
|
|
69
|
+
this.op = op;
|
|
70
|
+
this.start_timestamp = Date.now();
|
|
71
|
+
this.finish_timestamp = null;
|
|
72
|
+
this.duration_ms = 0;
|
|
73
|
+
this.status = "ok";
|
|
74
|
+
this.spans = [];
|
|
75
|
+
this.tags = {};
|
|
76
|
+
this.contexts = {};
|
|
77
|
+
this.user = null;
|
|
78
|
+
this.request = null;
|
|
79
|
+
this.release_version = null;
|
|
80
|
+
this.environment = APP_ENV;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
startSpan(op, description = "", data = {}) {
|
|
84
|
+
const parentSpanId = this.spans.length > 0 ? this.spans[this.spans.length - 1].span_id : null;
|
|
85
|
+
const spanId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
86
|
+
const span = new Span(spanId, op, description, parentSpanId, data);
|
|
87
|
+
this.spans.push(span);
|
|
88
|
+
return span;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
finish(status = "ok") {
|
|
92
|
+
this.finish_timestamp = Date.now();
|
|
93
|
+
this.duration_ms = this.finish_timestamp - this.start_timestamp;
|
|
94
|
+
this.status = status;
|
|
95
|
+
|
|
96
|
+
// Finish all unfinished spans
|
|
97
|
+
this.spans.forEach((span) => {
|
|
98
|
+
if (!span.finish_timestamp) {
|
|
99
|
+
span.finish();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Build trace payload
|
|
104
|
+
const payload = {
|
|
105
|
+
trace_id: this.trace_id,
|
|
106
|
+
transaction: this.transaction,
|
|
107
|
+
transaction_name: this.transaction_name,
|
|
108
|
+
start_timestamp: new Date(this.start_timestamp).toISOString(),
|
|
109
|
+
finish_timestamp: new Date(this.finish_timestamp).toISOString(),
|
|
110
|
+
duration_ms: this.duration_ms,
|
|
111
|
+
status: this.status,
|
|
112
|
+
environment: this.environment,
|
|
113
|
+
release: this.release_version,
|
|
114
|
+
spans: this.spans.map((s) => s.toJSON()),
|
|
115
|
+
tags: this.tags,
|
|
116
|
+
contexts: this.contexts,
|
|
117
|
+
user: this.user,
|
|
118
|
+
request: this.request,
|
|
119
|
+
platform: typeof window !== "undefined" ? "javascript" : "node",
|
|
120
|
+
sdk_name: "watchforge-javascript",
|
|
121
|
+
sdk_version: "0.1.0",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (DEBUG) {
|
|
125
|
+
console.log("[WatchForge Trace] Sending trace:", this.transaction_name);
|
|
126
|
+
console.log("[WatchForge Trace] Spans:", this.spans.length);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Send trace
|
|
130
|
+
if (DSN) {
|
|
131
|
+
sendTrace(DSN, payload);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return payload;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setTag(key, value) {
|
|
138
|
+
this.tags[key] = value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setUser(user) {
|
|
142
|
+
this.user = user;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setRequest(request) {
|
|
146
|
+
this.request = request;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sendTrace(dsn, payload) {
|
|
151
|
+
const isBrowser = typeof window !== "undefined";
|
|
152
|
+
const isNode = typeof process !== "undefined" && process.versions && process.versions.node;
|
|
153
|
+
|
|
154
|
+
// Parse DSN to get trace API URL
|
|
155
|
+
const dsnMatch = dsn.match(/^https?:\/\/([^@]+)@([^\/]+)\/(.+)$/);
|
|
156
|
+
if (!dsnMatch) {
|
|
157
|
+
console.error("WatchForge SDK: Invalid DSN");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const host = dsnMatch[2];
|
|
162
|
+
const scheme = dsn.startsWith("https") ? "https" : "http";
|
|
163
|
+
const finalScheme = host.includes("localhost") || host.includes("127.0.0.1") ? "http" : scheme;
|
|
164
|
+
const traceUrl = `${finalScheme}://${host}/api/ingestion/trace/`;
|
|
165
|
+
|
|
166
|
+
if (isBrowser) {
|
|
167
|
+
fetch(traceUrl, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `DSN ${dsn}`,
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify(payload),
|
|
174
|
+
keepalive: true,
|
|
175
|
+
}).catch((error) => {
|
|
176
|
+
console.error("WatchForge SDK - Failed to send trace:", error);
|
|
177
|
+
});
|
|
178
|
+
} else if (isNode) {
|
|
179
|
+
// Node.js: use http/https
|
|
180
|
+
const http = traceUrl.startsWith("https") ? require("https") : require("http");
|
|
181
|
+
const { URL } = require("url");
|
|
182
|
+
const url = new URL(traceUrl);
|
|
183
|
+
|
|
184
|
+
const bodyString = JSON.stringify(payload);
|
|
185
|
+
const bodyBuffer = Buffer.from(bodyString, "utf8");
|
|
186
|
+
|
|
187
|
+
let port = url.port;
|
|
188
|
+
if (!port || port === "") {
|
|
189
|
+
port = traceUrl.startsWith("https") ? 443 : 80;
|
|
190
|
+
} else {
|
|
191
|
+
port = parseInt(port, 10);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const options = {
|
|
195
|
+
hostname: url.hostname,
|
|
196
|
+
port: port,
|
|
197
|
+
path: url.pathname,
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
Authorization: `DSN ${dsn}`,
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
"Content-Length": bodyBuffer.length,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const req = http.request(options, (res) => {
|
|
207
|
+
let responseData = "";
|
|
208
|
+
res.on("data", (chunk) => {
|
|
209
|
+
responseData += chunk;
|
|
210
|
+
});
|
|
211
|
+
res.on("end", () => {
|
|
212
|
+
if (DEBUG) {
|
|
213
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
214
|
+
console.log(`✅ WatchForge SDK: Trace sent successfully (${res.statusCode})`);
|
|
215
|
+
} else {
|
|
216
|
+
console.error(`❌ WatchForge SDK: Trace failed (${res.statusCode}):`, responseData);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
req.on("error", (error) => {
|
|
223
|
+
if (DEBUG) {
|
|
224
|
+
console.error("❌ WatchForge SDK: Trace request error:", error.message);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
req.setTimeout(5000);
|
|
229
|
+
req.write(bodyBuffer);
|
|
230
|
+
req.end();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function startTransaction(transaction, transactionName = "", op = "http.server") {
|
|
235
|
+
const traceId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
236
|
+
const txn = new Transaction(traceId, transaction, transactionName, op);
|
|
237
|
+
transactionStack.push(txn);
|
|
238
|
+
return txn;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getCurrentTransaction() {
|
|
242
|
+
return transactionStack.length > 0 ? transactionStack[transactionStack.length - 1] : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function finishTransaction(status = "ok") {
|
|
246
|
+
const txn = transactionStack.pop();
|
|
247
|
+
if (txn) {
|
|
248
|
+
return txn.finish(status);
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { Transaction, Span };
|
package/src/transport.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Detect environment (browser vs Node.js)
|
|
2
|
+
const isBrowser = typeof window !== 'undefined';
|
|
3
|
+
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
|
|
4
|
+
|
|
5
|
+
// Lazy-load Node.js require function for ES modules
|
|
6
|
+
let nodeRequire = null;
|
|
7
|
+
let nodeRequirePromise = null;
|
|
8
|
+
|
|
9
|
+
function getNodeRequire() {
|
|
10
|
+
if (!isNode) return Promise.resolve(null);
|
|
11
|
+
if (nodeRequire) return Promise.resolve(nodeRequire);
|
|
12
|
+
|
|
13
|
+
// Initialize on first call (async, but we'll handle it in sendEvent)
|
|
14
|
+
if (!nodeRequirePromise) {
|
|
15
|
+
nodeRequirePromise = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
const { createRequire } = await import('module');
|
|
18
|
+
const { fileURLToPath } = await import('url');
|
|
19
|
+
nodeRequire = createRequire(fileURLToPath(import.meta.url));
|
|
20
|
+
return nodeRequire;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error("WatchForge SDK: Failed to create require function:", e);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return nodeRequirePromise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse DSN to extract components
|
|
32
|
+
function parseDsn(dsn) {
|
|
33
|
+
// DSN format: https://PUBLIC_KEY@HOST:PORT/PROJECT_ID
|
|
34
|
+
// or: https://PUBLIC_KEY@HOST/PROJECT_ID
|
|
35
|
+
try {
|
|
36
|
+
const dsnMatch = dsn.match(/^https?:\/\/([^@]+)@([^\/]+)\/(.+)$/);
|
|
37
|
+
if (dsnMatch) {
|
|
38
|
+
const publicKey = dsnMatch[1];
|
|
39
|
+
const host = dsnMatch[2];
|
|
40
|
+
const projectId = dsnMatch[3];
|
|
41
|
+
const scheme = dsn.startsWith('https') ? 'https' : 'http';
|
|
42
|
+
const finalScheme = (host.includes('localhost') || host.includes('127.0.0.1')) ? 'http' : scheme;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
scheme: finalScheme,
|
|
46
|
+
publicKey,
|
|
47
|
+
host,
|
|
48
|
+
projectId,
|
|
49
|
+
apiUrl: `${finalScheme}://${host}/api/ingestion/events/`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Invalid DSN
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Export parseDsn for use in client.js
|
|
59
|
+
export { parseDsn };
|
|
60
|
+
|
|
61
|
+
// Get API URL from DSN
|
|
62
|
+
function getApiUrl(dsn) {
|
|
63
|
+
// Priority: 1. Env var (for local dev override), 2. DSN-derived, 3. Default
|
|
64
|
+
// Check environment variable (for local development)
|
|
65
|
+
if (isNode && process.env.WATCHFORGE_API_URL) {
|
|
66
|
+
return process.env.WATCHFORGE_API_URL;
|
|
67
|
+
}
|
|
68
|
+
if (isBrowser && typeof window !== 'undefined' && window.WATCHFORGE_API_URL) {
|
|
69
|
+
return window.WATCHFORGE_API_URL;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse DSN to get API URL
|
|
73
|
+
const parsed = parseDsn(dsn);
|
|
74
|
+
if (parsed) {
|
|
75
|
+
return parsed.apiUrl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Default fallback
|
|
79
|
+
return isBrowser ? "/api/ingestion/events/" : "http://127.0.0.1:8001/api/ingestion/events/";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sendEvent(dsn, payload) {
|
|
83
|
+
const apiUrl = getApiUrl(dsn);
|
|
84
|
+
|
|
85
|
+
if (isBrowser) {
|
|
86
|
+
// Browser: use fetch
|
|
87
|
+
fetch(apiUrl, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `DSN ${dsn}`,
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(payload),
|
|
94
|
+
keepalive: true,
|
|
95
|
+
}).catch((error) => {
|
|
96
|
+
console.error("WatchForge SDK - Failed to send event:", error);
|
|
97
|
+
});
|
|
98
|
+
} else if (isNode) {
|
|
99
|
+
// Node.js: use http/https
|
|
100
|
+
// Handle async require initialization
|
|
101
|
+
const requirePromise = getNodeRequire();
|
|
102
|
+
// Fire and forget - don't block, but handle async
|
|
103
|
+
requirePromise.then((require) => {
|
|
104
|
+
if (!require) {
|
|
105
|
+
console.error("❌ WatchForge SDK: Failed to create require function");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Use require to load Node.js core modules
|
|
111
|
+
const http = apiUrl.startsWith('https')
|
|
112
|
+
? require('https')
|
|
113
|
+
: require('http');
|
|
114
|
+
const { URL } = require('url');
|
|
115
|
+
const url = new URL(apiUrl);
|
|
116
|
+
|
|
117
|
+
// Convert payload to JSON string
|
|
118
|
+
const bodyString = JSON.stringify(payload);
|
|
119
|
+
const bodyBuffer = Buffer.from(bodyString, 'utf8');
|
|
120
|
+
|
|
121
|
+
console.log("WatchForge SDK: Sending event to", apiUrl);
|
|
122
|
+
console.log("WatchForge SDK: Payload size:", bodyBuffer.length, "bytes");
|
|
123
|
+
|
|
124
|
+
// Parse port - url.port might be a string or empty
|
|
125
|
+
let port = url.port;
|
|
126
|
+
if (!port || port === '') {
|
|
127
|
+
port = apiUrl.startsWith('https') ? 443 : 80;
|
|
128
|
+
} else {
|
|
129
|
+
port = parseInt(port, 10);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const options = {
|
|
133
|
+
hostname: url.hostname,
|
|
134
|
+
port: port,
|
|
135
|
+
path: url.pathname,
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Authorization': `DSN ${dsn}`,
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'Content-Length': bodyBuffer.length,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const req = http.request(options, (res) => {
|
|
145
|
+
let responseData = '';
|
|
146
|
+
|
|
147
|
+
res.on('data', (chunk) => {
|
|
148
|
+
responseData += chunk;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
res.on('end', () => {
|
|
152
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
153
|
+
console.log(`✅ WatchForge SDK: Event sent successfully (${res.statusCode})`);
|
|
154
|
+
if (responseData) {
|
|
155
|
+
console.log("WatchForge SDK: Response:", responseData.substring(0, 200));
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
console.error(`❌ WatchForge SDK: Event failed (${res.statusCode}):`, responseData);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
req.on('error', (error) => {
|
|
164
|
+
console.error("❌ WatchForge SDK: Request error:", error.message);
|
|
165
|
+
console.error("WatchForge SDK: Error details:", {
|
|
166
|
+
code: error.code,
|
|
167
|
+
hostname: url.hostname,
|
|
168
|
+
port: url.port,
|
|
169
|
+
path: url.pathname,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
req.on('timeout', () => {
|
|
174
|
+
console.error("❌ WatchForge SDK: Request timeout");
|
|
175
|
+
req.destroy();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
req.setTimeout(5000); // 5 second timeout
|
|
179
|
+
|
|
180
|
+
// Write the body buffer
|
|
181
|
+
req.write(bodyBuffer);
|
|
182
|
+
req.end();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.error("❌ WatchForge SDK: Failed to send event:", e.message);
|
|
185
|
+
console.error("WatchForge SDK: Error stack:", e.stack);
|
|
186
|
+
}
|
|
187
|
+
}).catch((e) => {
|
|
188
|
+
console.error("❌ WatchForge SDK: Failed to initialize require:", e);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|