History

Trace first-party OAuth usage (#1633)

Rhys Sullivan committed 2 months ago

Commit deb39f71 parent c4432192 files · +52 −3Browse files

Unified · Split

2 files
  1. packages/core/sdk/src/oauth-first-party.test.ts +46 −3
  2. packages/core/sdk/src/oauth-service.ts +6 −0
packages/core/sdk/src/oauth-first-party.test.ts+46−3
@@ -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"); · 23 unmodified lines
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: { · 46 unmodified lines
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", () => { · 12 unmodified lines
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", () => { · 4 unmodified lines
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", () => { · 2 unmodified lines
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+6−0
@@ -1152,6 +1152,9 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { · 1151 unmodified lines
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 => { · 246 unmodified lines
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.