@getvision/server 0.0.0-develop-20251031183955

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/vision.ts ADDED
@@ -0,0 +1,319 @@
1
+ import type { Hono, Context, MiddlewareHandler } from 'hono'
2
+ import { VisionCore } from '@getvision/core'
3
+ import { AsyncLocalStorage } from 'async_hooks'
4
+ import { readFileSync, existsSync } from 'fs'
5
+ import { join } from 'path'
6
+
7
+ /**
8
+ * Vision context available in request handlers
9
+ */
10
+ interface VisionContext {
11
+ vision: VisionCore
12
+ traceId: string
13
+ rootSpanId: string
14
+ }
15
+
16
+ const visionContext = new AsyncLocalStorage<VisionContext>()
17
+
18
+ /**
19
+ * Get Vision context (available in route handlers)
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const { vision, traceId } = getVisionContext()
24
+ * const withSpan = vision.createSpanHelper(traceId)
25
+ * ```
26
+ */
27
+ export function getVisionContext(): VisionContext {
28
+ const context = visionContext.getStore()
29
+ if (!context) {
30
+ throw new Error('Vision context not available. Make sure Vision is enabled.')
31
+ }
32
+ return context
33
+ }
34
+
35
+ /**
36
+ * Create span helper using current trace context
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const withSpan = useVisionSpan()
41
+ * const users = withSpan('db.select', { 'db.table': 'users' }, () => {
42
+ * return db.select().from(users).all()
43
+ * })
44
+ * ```
45
+ */
46
+ export function useVisionSpan() {
47
+ const { vision, traceId, rootSpanId } = getVisionContext()
48
+ const tracer = vision.getTracer()
49
+
50
+ return <T>(
51
+ name: string,
52
+ attributes: Record<string, any> = {},
53
+ fn: () => T
54
+ ): T => {
55
+ const span = tracer.startSpan(name, traceId, rootSpanId)
56
+
57
+ for (const [key, value] of Object.entries(attributes)) {
58
+ tracer.setAttribute(span.id, key, value)
59
+ }
60
+
61
+ try {
62
+ const result = fn()
63
+ const completedSpan = tracer.endSpan(span.id)
64
+
65
+ if (completedSpan) {
66
+ vision.getTraceStore().addSpan(traceId, completedSpan)
67
+ }
68
+
69
+ return result
70
+ } catch (error) {
71
+ tracer.setAttribute(span.id, 'error', true)
72
+ tracer.setAttribute(
73
+ span.id,
74
+ 'error.message',
75
+ error instanceof Error ? error.message : String(error)
76
+ )
77
+ const completedSpan = tracer.endSpan(span.id)
78
+
79
+ if (completedSpan) {
80
+ vision.getTraceStore().addSpan(traceId, completedSpan)
81
+ }
82
+
83
+ throw error
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Vision middleware options
90
+ */
91
+ export interface VisionOptions {
92
+ enabled?: boolean
93
+ port?: number
94
+ maxTraces?: number
95
+ maxLogs?: number
96
+ logging?: boolean
97
+ service?: {
98
+ name?: string
99
+ version?: string
100
+ description?: string
101
+ }
102
+ }
103
+
104
+ let visionInstance: VisionCore | null = null
105
+
106
+ /**
107
+ * Get Vision instance
108
+ */
109
+ export function getVisionInstance(): VisionCore | null {
110
+ return visionInstance
111
+ }
112
+
113
+ /**
114
+ * Auto-detect package.json info
115
+ */
116
+ function autoDetectPackageInfo(): { name: string; version: string } {
117
+ try {
118
+ const pkgPath = join(process.cwd(), 'package.json')
119
+ if (existsSync(pkgPath)) {
120
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
121
+ return {
122
+ name: pkg.name || 'unknown',
123
+ version: pkg.version || '0.0.0',
124
+ }
125
+ }
126
+ } catch (error) {
127
+ // Ignore errors
128
+ }
129
+ return { name: 'unknown', version: '0.0.0' }
130
+ }
131
+
132
+ /**
133
+ * Create Vision middleware for Hono
134
+ *
135
+ * This middleware automatically:
136
+ * - Creates traces for all requests
137
+ * - Adds request/response metadata
138
+ * - Provides Vision context to handlers
139
+ * - Broadcasts events to Vision Dashboard
140
+ */
141
+ export function createVisionMiddleware(options: VisionOptions = {}): MiddlewareHandler {
142
+ const {
143
+ enabled = true,
144
+ port = 9500,
145
+ maxTraces = 1000,
146
+ maxLogs = 10000,
147
+ logging = true
148
+ } = options
149
+
150
+ if (!enabled) {
151
+ return async (c, next) => await next()
152
+ }
153
+
154
+ // Initialize Vision Core once
155
+ if (!visionInstance) {
156
+ visionInstance = new VisionCore({ port, maxTraces, maxLogs })
157
+
158
+ // Auto-detect service info
159
+ const pkgInfo = autoDetectPackageInfo()
160
+ const serviceName = options.service?.name || pkgInfo.name
161
+ const serviceVersion = options.service?.version || pkgInfo.version
162
+
163
+ // Set app status
164
+ visionInstance.setAppStatus({
165
+ name: serviceName,
166
+ version: serviceVersion,
167
+ description: options.service?.description,
168
+ running: true,
169
+ pid: process.pid,
170
+ metadata: {
171
+ framework: 'vision-sdk',
172
+ },
173
+ })
174
+ }
175
+
176
+ const vision = visionInstance
177
+
178
+ // Middleware to trace requests
179
+ return async (c: Context, next) => {
180
+ // Skip OPTIONS requests
181
+ if (c.req.method === 'OPTIONS') {
182
+ return next()
183
+ }
184
+
185
+ const startTime = Date.now()
186
+
187
+ // Create trace
188
+ const trace = vision.createTrace(c.req.method, c.req.path)
189
+
190
+ // Run request in AsyncLocalStorage context
191
+ return visionContext.run({ vision, traceId: trace.id, rootSpanId: '' }, async () => {
192
+ // Start main span
193
+ const tracer = vision.getTracer()
194
+ const rootSpan = tracer.startSpan('http.request', trace.id)
195
+
196
+ // Update context with rootSpanId
197
+ const ctx = visionContext.getStore()
198
+ if (ctx) {
199
+ ctx.rootSpanId = rootSpan.id
200
+ }
201
+
202
+ // Add request attributes
203
+ tracer.setAttribute(rootSpan.id, 'http.method', c.req.method)
204
+ tracer.setAttribute(rootSpan.id, 'http.path', c.req.path)
205
+ tracer.setAttribute(rootSpan.id, 'http.url', c.req.url)
206
+
207
+ // Add query params if any
208
+ const url = new URL(c.req.url)
209
+ if (url.search) {
210
+ tracer.setAttribute(rootSpan.id, 'http.query', url.search)
211
+ }
212
+
213
+ // Capture request metadata
214
+ try {
215
+ const rawReq = c.req.raw
216
+ const headers: Record<string, string> = {}
217
+ rawReq.headers.forEach((v, k) => { headers[k] = v })
218
+
219
+ const urlObj = new URL(c.req.url)
220
+ const query: Record<string, string> = {}
221
+ urlObj.searchParams.forEach((v, k) => { query[k] = v })
222
+
223
+ let body: unknown = undefined
224
+ const ct = headers['content-type'] || headers['Content-Type']
225
+ if (ct && ct.includes('application/json')) {
226
+ try {
227
+ body = await rawReq.clone().json()
228
+ } catch {}
229
+ }
230
+
231
+ const sessionId = headers['x-vision-session']
232
+ if (sessionId) {
233
+ tracer.setAttribute(rootSpan.id, 'session.id', sessionId)
234
+ trace.metadata = { ...(trace.metadata || {}), sessionId }
235
+ }
236
+
237
+ const requestMeta = {
238
+ method: c.req.method,
239
+ url: urlObj.pathname + (urlObj.search || ''),
240
+ headers,
241
+ query: Object.keys(query).length ? query : undefined,
242
+ body,
243
+ }
244
+ tracer.setAttribute(rootSpan.id, 'http.request', requestMeta)
245
+ trace.metadata = { ...(trace.metadata || {}), request: requestMeta }
246
+
247
+ // Emit start log
248
+ if (logging) {
249
+ const parts = [
250
+ `method=${c.req.method}`,
251
+ `path=${c.req.path}`,
252
+ ]
253
+ if (sessionId) parts.push(`sessionId=${sessionId}`)
254
+ parts.push(`traceId=${trace.id}`)
255
+ console.info(`INF starting request ${parts.join(' ')}`)
256
+ }
257
+
258
+ // Execute request
259
+ await next()
260
+
261
+ // Add response attributes
262
+ tracer.setAttribute(rootSpan.id, 'http.status_code', c.res.status)
263
+ const resHeaders: Record<string, string> = {}
264
+ c.res.headers?.forEach((v, k) => { resHeaders[k] = v as unknown as string })
265
+
266
+ let respBody: unknown = undefined
267
+ const resCt = c.res.headers?.get('content-type') || ''
268
+ try {
269
+ const clone = c.res.clone()
270
+ if (resCt.includes('application/json')) {
271
+ const txt = await clone.text()
272
+ if (txt && txt.length <= 65536) {
273
+ try { respBody = JSON.parse(txt) } catch { respBody = txt }
274
+ }
275
+ }
276
+ } catch {}
277
+
278
+ const responseMeta = {
279
+ status: c.res.status,
280
+ headers: Object.keys(resHeaders).length ? resHeaders : undefined,
281
+ body: respBody,
282
+ }
283
+ tracer.setAttribute(rootSpan.id, 'http.response', responseMeta)
284
+ trace.metadata = { ...(trace.metadata || {}), response: responseMeta }
285
+
286
+ } catch (error) {
287
+ // Track error
288
+ tracer.addEvent(rootSpan.id, 'error', {
289
+ message: error instanceof Error ? error.message : 'Unknown error',
290
+ stack: error instanceof Error ? error.stack : undefined,
291
+ })
292
+
293
+ tracer.setAttribute(rootSpan.id, 'error', true)
294
+ throw error
295
+
296
+ } finally {
297
+ // End span and add it to trace
298
+ const completedSpan = tracer.endSpan(rootSpan.id)
299
+ if (completedSpan) {
300
+ vision.getTraceStore().addSpan(trace.id, completedSpan)
301
+ }
302
+
303
+ // Complete trace
304
+ const duration = Date.now() - startTime
305
+ vision.completeTrace(trace.id, c.res.status, duration)
306
+
307
+ // Add trace ID to response headers
308
+ c.header('X-Vision-Trace-Id', trace.id)
309
+
310
+ // Emit completion log
311
+ if (logging) {
312
+ console.info(
313
+ `INF request completed code=${c.res.status} duration=${duration}ms method=${c.req.method} path=${c.req.path} traceId=${trace.id}`
314
+ )
315
+ }
316
+ }
317
+ })
318
+ }
319
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@repo/typescript-config/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }