Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/honest-canyons-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"apollo": patch
---

Add version metadata to HTTP headers, chat payloads (meta key), and langfuse
traces
5 changes: 5 additions & 0 deletions .changeset/two-flowers-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apollo": patch
---

For langfuse traces with code, add a `has_code_attachment` tag
18 changes: 8 additions & 10 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@ Fixes #

## Implementation Details

A more detailed breakdown of the changes, including motivations (if not provided in the issue).
A more detailed breakdown of the changes, including motivations (if not provided
in the issue).

## AI Usage

Please disclose how you've used AI in this work (it's cool, we just want to know!):
Please disclose whether you've used AI in this work (it's cool, we just want to
know!):

- [ ] Code generation (copilot but not intellisense)
- [ ] Learning or fact checking
- [ ] Strategy / design
- [ ] Optimisation / refactoring
- [ ] Translation / spellchecking / doc gen
- [ ] Other
- [ ] I have not used AI
- [ ] Yes, I have not used AI
- [ ] No, I have not used AI

You can read more details in our [Responsible AI Policy](https://www.openfn.org/ai#pull-request-templates)
You can read more details in our
[Responsible AI Policy](https://www.openfn.org/ai#pull-request-templates)
9 changes: 8 additions & 1 deletion platform/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "node:path";
import { spawn } from "node:child_process";
import { rm } from "node:fs/promises";
import { getInternalToken } from "./auth/internal-token";
import pkg from "../../package.json";

/**
Run a python script
Expand Down Expand Up @@ -46,7 +47,13 @@ export const run = async (
// Hand the internal token to the child explicitly so its apollo() self-calls
// are recognised by the auth hook. Spawned from here (the honest owner) rather than
// written back onto this process's env.
{ env: { ...process.env, APOLLO_INTERNAL_TOKEN: getInternalToken() } }
{
env: {
...process.env,
APOLLO_INTERNAL_TOKEN: getInternalToken(),
APOLLO_VERSION: pkg.version,
},
}
);

proc.on("error", async (err) => {
Expand Down
2 changes: 2 additions & 0 deletions platform/src/middleware/dir.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default async (app: Elysia) => {
// jsx templates!
// https://elysiajs.com/patterns/mvc.html#view

app.head("/", () => new Response(null, { status: 200 }));

const modules = await describeModules(path.resolve("./services"));
app.get("/", () => {
return (
Expand Down
2 changes: 2 additions & 0 deletions platform/src/middleware/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export default async (app: Elysia, port: number, auth: InstanceAuth) => {
const { name, readme } = m;
console.log(" - mounted /services/" + name);

app.head(name, () => new Response(null, { status: 200 }));

// simple post
app.post(name, async (ctx) => {
console.log(`POST /services/${name}: ${ctx.uuid}`);
Expand Down
2 changes: 2 additions & 0 deletions platform/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { captureException } from "./util/sentry";
import { clientsDbUrl, closeDb } from "./db";
import { runMigrations } from "./db/migrate";
import { randomUUID } from "node:crypto";
import pkg from "../../package.json";

export default async (
port: number | string = 3000,
Expand All @@ -23,6 +24,7 @@ export default async (
app.use(html());

app.derive(() => ({ start: Date.now(), uuid: randomUUID() }));
app.onAfterHandle(({ set }) => { set.headers["X-Api-Version"] = pkg.version; });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think onAfterHandle only fires on the success path, so I'm not sure the header actually lands on all responses like the description says. Errors go through onError which doesn't set it, so wouldn't 401s from the auth hook and ApolloError/500s come back without the version? And for the streamed /services/*/stream responses, aren't the headers already flushed by the time this runs?

Not a blocker at all since the breaking-change guard rides on meta.apollo_version anyway. But should we set it somewhere that also covers the error/stream paths (mirror it in onError, or move it to onRequest/mapResponse)? And maybe add a test for an error response too, since the current one only checks the happy path?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch - thanks I'll take a close look at this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've just added a 400 response from python and that includes the header

$ curl http://localhost:3000/services/echo  -v -X POST
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> POST /services/echo HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< X-Api-Version: 1.4.0
< Date: Fri, 26 Jun 2026 14:00:29 GMT
< Content-Length: 59

But in this case the typescript handler returns successfully. So if the handler threw an internal error for some reason we might not get the version number.

You make a good point about streaming though. I'm not actually sure where we'd return the version for a streaming API. I think I'll raise an issue for this.

One thing to note is that Lightning will make a HEAD call to the endpoint to get the version before it does anything. So the streaming thing is academic

app.onAfterHandle(logRequest);

// Report unhandled throws to Sentry, then return nothing so Elysia produces
Expand Down
Loading