-
-
Notifications
You must be signed in to change notification settings - Fork 29
Add a CLI that agents can also use instead of the MCP #997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 13 commits
241b575
bd92067
7fd359f
1d5861c
db12a52
383272a
29dc7af
e48ed17
9ea35f3
1015034
e0ba603
78925cd
a758f1e
6d2d0d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@spotlightjs/sidecar": minor | ||
"@spotlightjs/spotlight": minor | ||
--- | ||
|
||
Add CLI -- `spotlight logs+errors` etc are now available |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
"buildx", | ||
"codesign", | ||
"contextlines", | ||
"decompressors", | ||
"Endcaps", | ||
"fontsource", | ||
"getsentry", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,2 @@ | ||
export const DEFAULT_SIDECAR_URL = "http://localhost:8969"; | ||
export const DEFAULT_SIDECAR_STREAM_URL = new URL("/stream", DEFAULT_SIDECAR_URL).href; | ||
export const DEFAULT_INITIAL_TAB = "/traces"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,40 @@ | ||
#!/usr/bin/env node | ||
import { captureException } from "@sentry/core"; | ||
import { EventSource } from "eventsource"; | ||
import { SENTRY_CONTENT_TYPE } from "./src/constants.js"; | ||
import { formatEnvelope } from "./src/format"; | ||
import { parseCLIArgs, setupSidecar } from "./src/main.js"; | ||
import type { ParsedEnvelope } from "./src/parser/processEnvelope.js"; | ||
|
||
const args = parseCLIArgs(); | ||
const connectUpstream = async (port: number) => | ||
new Promise<EventSource>((resolve, reject) => { | ||
const client = new EventSource(`http://localhost:${port}/stream`); | ||
client.onerror = reject; | ||
client.onopen = () => resolve(client); | ||
}); | ||
|
||
if (args.help) { | ||
const SEPARATOR = Array(10).fill("─").join(""); | ||
|
||
function displayEnvelope(envelope: ParsedEnvelope["envelope"]) { | ||
console.log(`${envelope[0].event_id} | ${envelope[1][0][0].type} | ${envelope[0].sdk?.name}\n\n`); | ||
const lines = formatEnvelope(envelope); | ||
if (lines.length > 0) { | ||
console.log(lines.join("")); | ||
} else { | ||
console.log("No parser for the given event type"); | ||
} | ||
console.log("\n"); | ||
console.log(SEPARATOR); | ||
} | ||
|
||
const printHelp = () => { | ||
console.log(` | ||
Spotlight Sidecar - Development proxy server for Spotlight | ||
|
||
Usage: spotlight-sidecar [options] | ||
|
||
Options: | ||
-p, --port <port> Port to listen on (default: 8969) | ||
--stdio-mcp Enable MCP stdio transport | ||
-d, --debug Enable debug logging | ||
-h, --help Show this help message | ||
|
||
|
@@ -21,9 +44,94 @@ Examples: | |
spotlight-sidecar -p 3000 -d # Start on port 3000 with debug logging | ||
`); | ||
process.exit(0); | ||
}; | ||
|
||
let runServer = true; | ||
const args = parseCLIArgs(); | ||
let stdioMCP = false; | ||
|
||
if (args.help) { | ||
runServer = false; | ||
printHelp(); | ||
} | ||
|
||
await setupSidecar({ | ||
...args, | ||
isStandalone: true, | ||
let onEnvelope: ((envelope: ParsedEnvelope["envelope"]) => void) | undefined = undefined; | ||
const NAME_TO_TYPE_MAPPING: Record<string, string[]> = Object.freeze({ | ||
traces: ["transaction", "span"], | ||
profiles: ["profile"], | ||
logs: ["log"], | ||
attachments: ["attachment"], | ||
errors: ["event"], | ||
sessions: ["session"], | ||
replays: ["replay_video"], | ||
// client_report | ||
}); | ||
const EVERYTHING_MAGIC_WORDS = new Set(["everything", "all", "*"]); | ||
export const SUPPORTED_ARGS = new Set([...Object.keys(NAME_TO_TYPE_MAPPING), ...EVERYTHING_MAGIC_WORDS]); | ||
|
||
const cmd = args._positionals[0]; | ||
switch (cmd) { | ||
case "help": | ||
printHelp(); | ||
runServer = false; | ||
break; | ||
case "mcp": | ||
stdioMCP = true; | ||
break; | ||
case "run": | ||
// do crazy stuff | ||
break; | ||
case undefined: | ||
case "": | ||
break; | ||
default: { | ||
if (args._positionals.length > 1) { | ||
console.error("Error: Too many positional arguments."); | ||
printHelp(); | ||
} | ||
const eventTypes = cmd.toLowerCase().split(/\s*[,+]\s*/gi); | ||
betegon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
for (const eventType of eventTypes) { | ||
if (!SUPPORTED_ARGS.has(eventType)) { | ||
console.error(`Error: Unsupported argument "${eventType}".`); | ||
console.error(`Supported arguments are: ${[...SUPPORTED_ARGS].join(", ")}`); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
if (eventTypes.some(type => EVERYTHING_MAGIC_WORDS.has(type))) { | ||
onEnvelope = displayEnvelope; | ||
} else { | ||
const types = new Set([...eventTypes.flatMap(type => NAME_TO_TYPE_MAPPING[type] || [])]); | ||
onEnvelope = envelope => { | ||
for (const [header] of envelope[1]) { | ||
if (header.type && types.has(header.type)) { | ||
displayEnvelope(envelope); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
// try to connect to an already existing server first | ||
betegon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
try { | ||
const client = await connectUpstream(args.port); | ||
runServer = false; | ||
client.addEventListener(SENTRY_CONTENT_TYPE, event => onEnvelope!(JSON.parse(event.data))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Callback Type Mismatch and Safety IssuesThe Additional Locations (1) |
||
} catch (err) { | ||
// if we fail, fine then we'll start our own | ||
if (err instanceof Error && !err.message?.includes(args.port.toString())) { | ||
captureException(err); | ||
console.error("Error when trying to connect to upstream sidecar:", err); | ||
process.exit(1); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Event Parsing Fails; Error Handling InvertedThe |
||
} | ||
} | ||
|
||
if (runServer) { | ||
await setupSidecar({ | ||
...args, | ||
stdioMCP, | ||
onEnvelope, | ||
isStandalone: true, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type { ErrorEvent } from "@sentry/core"; | ||
import { | ||
isErrorEvent, | ||
isLogEvent, | ||
type SentryLogEvent, | ||
type ParsedEnvelope, | ||
type SentryEvent, | ||
} from "~/parser/index.js"; | ||
import { processErrorEvent } from "./errors.js"; | ||
import { formatEventOutput } from "./event.js"; | ||
import { processLogEvent } from "./logs.js"; | ||
|
||
export const eventHandlers = { | ||
error: (payload: ErrorEvent) => formatEventOutput(processErrorEvent(payload)), | ||
log: (payload: SentryLogEvent) => { | ||
const content: string[] = []; | ||
for (const log of payload.items) { | ||
content.push(processLogEvent(log)); | ||
} | ||
return content; | ||
}, | ||
}; | ||
|
||
export function formatEnvelope(envelope: ParsedEnvelope["envelope"]): string[] { | ||
const [, items] = envelope; | ||
|
||
const formatted: string[] = []; | ||
for (const item of items) { | ||
const [{ type }, payload] = item; | ||
|
||
if (type === "event" && isErrorEvent(payload as SentryEvent)) { | ||
formatted.push(eventHandlers.error(payload as ErrorEvent)); | ||
} else if (type === "log" && isLogEvent(payload as SentryEvent)) { | ||
formatted.push(...eventHandlers.log(payload as SentryLogEvent)); | ||
} | ||
} | ||
|
||
return formatted; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential bug: The code does not validate the result of
envelope.getParsedEnvelope()
, leading to a crash if the envelope is malformed (null) or empty (empty items array).Description: The function
envelope.getParsedEnvelope()
can returnnull
if the envelope header is malformed. The code destructures this result viaconst { event } = envelope.getParsedEnvelope()
without a null check, which will cause the process to crash. Additionally, even with a valid envelope, if it contains no items, theevent[1]
array will be empty. The subsequent accessevent[1][0][0].type
will then cause a crash due to an out-of-bounds access. Both scenarios, a malformed header or a valid but empty envelope, will lead to a server crash.Suggested fix: Before destructuring or accessing properties, check if the return value of
envelope.getParsedEnvelope()
is null. Also, add a check to ensure theevent[1]
array (the envelope items) is not empty before attempting to access its elements. A guard clause likeif (!envelope || !event || event[1].length === 0) { return; }
would prevent both crashes.severity: 0.9, confidence: 0.95
Did we get this right? 👍 / 👎 to inform future reviews.