Skip to content

Commit deb39f7

Browse files
authored
Trace first-party OAuth usage (#1633)
1 parent c443219 commit deb39f7

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

packages/core/sdk/src/oauth-first-party.test.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from "@effect/vitest";
22
import { Effect, Predicate } from "effect";
3+
import type * as Tracer from "effect/Tracer";
34

45
import {
56
AuthTemplateSlug,
@@ -29,6 +30,37 @@ const INTEG = IntegrationSlug.make("acme");
2930
const TEMPLATE = AuthTemplateSlug.make("oauth");
3031
const FIRST_PARTY = firstPartyOAuthClientSlug("acme");
3132

33+
const makeRecordingTracer = (spans: Map<string, Map<string, unknown>>): Tracer.Tracer => ({
34+
span: (options) => {
35+
const attributes = new Map<string, unknown>();
36+
spans.set(options.name, attributes);
37+
let status: Tracer.SpanStatus = { _tag: "Started", startTime: options.startTime };
38+
return {
39+
_tag: "Span",
40+
name: options.name,
41+
spanId: "0000000000000001",
42+
traceId: "00000000000000000000000000000001",
43+
parent: options.parent,
44+
annotations: options.annotations,
45+
get status() {
46+
return status;
47+
},
48+
attributes,
49+
links: options.links,
50+
sampled: options.sampled,
51+
kind: options.kind,
52+
end: (endTime, exit) => {
53+
status = { _tag: "Ended", startTime: options.startTime, endTime, exit };
54+
},
55+
attribute: (key, value) => {
56+
attributes.set(key, value);
57+
},
58+
event: () => undefined,
59+
addLinks: () => undefined,
60+
};
61+
},
62+
});
63+
3264
const oauthPlugin = definePlugin(() => ({
3365
id: "acme" as const,
3466
storage: () => ({}),
@@ -81,8 +113,9 @@ const firstPartyClientFor = (server: {
81113
describe("first-party oauth clients", () => {
82114
it.effect(
83115
"start → complete through a config-declared client mints an executable connection",
84-
() =>
85-
Effect.scoped(
116+
() => {
117+
const spans = new Map<string, Map<string, unknown>>();
118+
return Effect.scoped(
86119
Effect.gen(function* () {
87120
const server = yield* serveOAuthTestServer({ scopes: ["read"] });
88121
const { executor } = yield* makeTestWorkspaceHarness({
@@ -101,6 +134,9 @@ describe("first-party oauth clients", () => {
101134
template: TEMPLATE,
102135
});
103136
expect(started.status).toBe("redirect");
137+
expect(
138+
spans.get("test.oauth.first_party")?.get("executor.oauth.client_first_party"),
139+
).toBe(true);
104140
if (started.status !== "redirect") return;
105141

106142
const callback = yield* server.completeAuthorizationCodeFlow({
@@ -111,6 +147,9 @@ describe("first-party oauth clients", () => {
111147
code: callback.code,
112148
});
113149
expect(String(connection.address)).toBe("tools.acme.org.mainAccount");
150+
expect(
151+
spans.get("executor.oauth.complete")?.get("executor.oauth.client_first_party"),
152+
).toBe(true);
114153

115154
const out = (yield* executor.execute(
116155
ToolAddress.make("tools.acme.org.mainAccount.whoami"),
@@ -119,7 +158,11 @@ describe("first-party oauth clients", () => {
119158
expect(out.token).toMatch(/^at_/);
120159
expect(yield* server.acceptsAccessToken(out.token)).toBe(true);
121160
}),
122-
),
161+
).pipe(
162+
Effect.withSpan("test.oauth.first_party"),
163+
Effect.withTracer(makeRecordingTracer(spans)),
164+
);
165+
},
123166
);
124167

125168
it.effect("refresh resolves the config-declared client (no oauth_client row exists)", () =>

packages/core/sdk/src/oauth-service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,9 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
11521152
// First-party apps are deployment-owned, outside the owner lattice
11531153
// entirely, so the rule does not apply to them.
11541154
const firstPartyFlow = isFirstPartyOAuthClientSlug(String(input.client));
1155+
yield* Effect.annotateCurrentSpan({
1156+
"executor.oauth.client_first_party": firstPartyFlow,
1157+
});
11551158
if (!firstPartyFlow && input.owner === "org" && input.clientOwner === "user") {
11561159
return yield* new OAuthStartError({
11571160
message: "A Workspace connection must use a Workspace app.",
@@ -1404,6 +1407,9 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => {
14041407
"executor.connection": String(session.name),
14051408
"executor.template": String(session.template),
14061409
"executor.oauth.client": String(session.clientSlug),
1410+
"executor.oauth.client_first_party": isFirstPartyOAuthClientSlug(
1411+
String(session.clientSlug),
1412+
),
14071413
});
14081414

14091415
// Expired sessions are not redeemable — drop + treat as not found.

0 commit comments

Comments
 (0)