Skip to content

Commit 55a8b5e

Browse files
smrhtRhysSullivan
andauthored
fix(openapi): fetch analyticsdata Discovery from the service host (#1955)
* fix(openapi): fetch analyticsdata Discovery from the service host The central Discovery directory does not list the GA4 Data API, so https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest answers 404 and importing the source fails. Route analyticsdata to its own host the way forms, keep and photospicker already are. * Test queue timeout with a controlled clock * Verify Analytics Data discovery import end to end --------- Co-authored-by: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
1 parent 85cf428 commit 55a8b5e

5 files changed

Lines changed: 87 additions & 1 deletion

File tree

‎.changeset/great-hoops-repeat.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@executor-js/plugin-openapi": patch
3+
---
4+
5+
Fetch Google Analytics Data (`analyticsdata`) Discovery from the service's own
6+
host. The central directory does not list the GA4 Data API, so the canonical
7+
`https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest` answers
8+
404 and the source fails to import. Same treatment `forms`, `keep` and
9+
`photospicker` already get.

‎apps/cloud/src/mcp/session-build-semaphore.test.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, beforeEach } from "@effect/vitest";
1+
import { describe, expect, it, beforeEach, afterEach, vi } from "@effect/vitest";
22

33
import {
44
acquireBuildSlot,
@@ -13,6 +13,10 @@ describe("session-build-semaphore", () => {
1313
resetBuildSlotsForTest();
1414
});
1515

16+
afterEach(() => {
17+
vi.useRealTimers();
18+
});
19+
1620
it("grants up to the cap immediately, with no wait", async () => {
1721
const results = await Promise.all([
1822
acquireBuildSlot().promise,
@@ -214,6 +218,7 @@ describe("session-build-semaphore", () => {
214218
});
215219

216220
it("proceeds without a slot when the queue wait exceeds the timeout, and does not count it as active", async () => {
221+
vi.useFakeTimers();
217222
await Promise.all([
218223
acquireBuildSlot().promise,
219224
acquireBuildSlot().promise,
@@ -223,6 +228,10 @@ describe("session-build-semaphore", () => {
223228
expect(currentActiveBuildsForTest()).toBe(4);
224229

225230
const timedOutHandle = acquireBuildSlot(10);
231+
await vi.advanceTimersByTimeAsync(9);
232+
expect(currentQueueLengthForTest()).toBe(1);
233+
expect(currentActiveBuildsForTest()).toBe(4);
234+
await vi.advanceTimersByTimeAsync(1);
226235
const result = await timedOutHandle.promise;
227236

228237
expect(result).toEqual({ acquired: false, waitMs: expect.any(Number), timedOut: true });
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { randomBytes, randomUUID } from "node:crypto";
2+
import { expect } from "@effect/vitest";
3+
import { Effect } from "effect";
4+
import { composePluginApi } from "@executor-js/api/server";
5+
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
6+
import {
7+
AuthTemplateSlug,
8+
ConnectionName,
9+
IntegrationSlug,
10+
ProviderItemId,
11+
} from "@executor-js/sdk/shared";
12+
13+
import { scenario } from "../src/scenario";
14+
import { Api, Target } from "../src/services";
15+
16+
const api = composePluginApi([openApiHttpPlugin()] as const);
17+
18+
scenario(
19+
"Google Analytics Data · public Discovery import exposes report tools",
20+
{ timeout: 120_000 },
21+
Effect.gen(function* () {
22+
const target = yield* Target;
23+
const { client } = yield* Api;
24+
const identity = yield* target.newIdentity();
25+
const apiClient = yield* client(api, identity);
26+
const slug = IntegrationSlug.make(`analytics_${randomBytes(4).toString("hex")}`);
27+
// Fetch Google's public document through the real server import path.
28+
// No Analytics account or authenticated report request is needed.
29+
const added = yield* apiClient.openapi.addSpec({
30+
payload: {
31+
slug,
32+
spec: {
33+
kind: "url",
34+
url: "https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest",
35+
},
36+
},
37+
});
38+
yield* Effect.gen(function* () {
39+
expect(added.toolCount).toBeGreaterThan(0);
40+
const providers = yield* apiClient.providers.list();
41+
const provider = providers[0];
42+
if (provider === undefined) return yield* Effect.die("No credential provider available");
43+
yield* apiClient.connections.create({
44+
payload: {
45+
owner: "org",
46+
name: ConnectionName.make("main"),
47+
integration: slug,
48+
template: AuthTemplateSlug.make("googleOAuth2"),
49+
from: { provider, id: ProviderItemId.make(randomUUID()) },
50+
},
51+
});
52+
const tools = yield* apiClient.tools.list({ query: { integration: slug } });
53+
expect(tools.some((tool) => tool.name.endsWith("runReport"))).toBe(true);
54+
expect(tools.some((tool) => tool.name.endsWith("runRealtimeReport"))).toBe(true);
55+
}).pipe(Effect.ensuring(apiClient.openapi.removeSpec({ params: { slug } }).pipe(Effect.orDie)));
56+
}),
57+
);

‎packages/plugins/openapi/src/providers/google/discovery.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ it("accepts only supported HTTPS Google Discovery endpoints", () => {
159159
"https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest",
160160
),
161161
).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1");
162+
expect(
163+
normalizeGoogleDiscoveryUrl(
164+
"https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest",
165+
),
166+
).toBe("https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta");
162167
expect(
163168
normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/forms/v1/rest"),
164169
).toBe("https://forms.googleapis.com/$discovery/rest?version=v1");
@@ -171,6 +176,11 @@ it("accepts only supported HTTPS Google Discovery endpoints", () => {
171176
expect(
172177
normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"),
173178
).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1");
179+
expect(
180+
normalizeGoogleDiscoveryUrl(
181+
"https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta",
182+
),
183+
).toBe("https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta");
174184
expect(
175185
normalizeGoogleDiscoveryUrl("https://forms.googleapis.com/$discovery/rest?version=v1"),
176186
).toBe("https://forms.googleapis.com/$discovery/rest?version=v1");

‎packages/plugins/openapi/src/providers/google/discovery.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type GoogleDiscoveryServiceOverride = {
4141
};
4242

4343
const GOOGLE_DISCOVERY_SERVICE_OVERRIDES: Record<string, GoogleDiscoveryServiceOverride> = {
44+
analyticsdata: { preserveServiceHostedUrl: true },
4445
forms: { preserveServiceHostedUrl: true },
4546
keep: { preserveServiceHostedUrl: true },
4647
[GOOGLE_PHOTOS_PICKER_SERVICE]: {

0 commit comments

Comments
 (0)