@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/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 };
@@ -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
+ }