diff --git a/packages/appkit/src/connectors/lakebase/tests/initialize-pool.test.ts b/packages/appkit/src/connectors/lakebase/tests/initialize-pool.test.ts index 29715fce4..3912e2c71 100644 --- a/packages/appkit/src/connectors/lakebase/tests/initialize-pool.test.ts +++ b/packages/appkit/src/connectors/lakebase/tests/initialize-pool.test.ts @@ -55,7 +55,7 @@ describe("AppKit Lakebase connector initialization", () => { expect(await initializeLakebasePool()).toBe(pool); expect(mocks.me).toHaveBeenCalledOnce(); expect(mocks.createWorkspaceClient).toHaveBeenCalledWith({ - clientOptions: expect.objectContaining({ product: "@databricks/appkit" }), + clientOptions: expect.objectContaining({ product: "databricks-appkit" }), }); expect(mocks.createPool).toHaveBeenCalledWith({ user: "local-user@example.test", diff --git a/packages/appkit/src/context/client-options.ts b/packages/appkit/src/context/client-options.ts index c4a750d4d..1d6ee3888 100644 --- a/packages/appkit/src/context/client-options.ts +++ b/packages/appkit/src/context/client-options.ts @@ -1,17 +1,17 @@ import { coerce } from "semver"; -import { - name as productName, - version as productVersion, -} from "../../package.json"; +import { version as productVersion } from "../../package.json"; import type { ClientOptions } from "../workspace-client"; const normalizedVersion = (coerce(productVersion)?.version ?? productVersion) as ClientOptions["productVersion"]; +/** The product name AppKit stamps on outbound requests (not the npm package name). */ +const productName = "databricks-appkit"; + /** * SDK client options that stamp every `apiClient.request()` with an AppKit - * User-Agent (`@databricks/appkit/`), so outbound Databricks traffic + * User-Agent (`databricks-appkit/`), so outbound Databricks traffic * is attributable to AppKit. Use this for every `WorkspaceClient` AppKit * constructs at runtime. */ diff --git a/packages/appkit/src/context/tests/service-context.test.ts b/packages/appkit/src/context/tests/service-context.test.ts index f18593f54..d3b3cd5cd 100644 --- a/packages/appkit/src/context/tests/service-context.test.ts +++ b/packages/appkit/src/context/tests/service-context.test.ts @@ -629,7 +629,7 @@ describe("ServiceContext", () => { test("should return product name and version", () => { const options = ServiceContext.getClientOptions(); - expect(options.product).toBe("@databricks/appkit"); + expect(options.product).toBe("databricks-appkit"); expect(options.productVersion).toBeDefined(); }); diff --git a/packages/appkit/src/context/tests/user-agent-wire.integration.test.ts b/packages/appkit/src/context/tests/user-agent-wire.integration.test.ts index c559d4f55..a8895df87 100644 --- a/packages/appkit/src/context/tests/user-agent-wire.integration.test.ts +++ b/packages/appkit/src/context/tests/user-agent-wire.integration.test.ts @@ -70,9 +70,7 @@ describe("User-Agent attribution (wire)", () => { // A real server received it, so this is the transport's User-Agent, not a // mocked value. The raw seam sends AppKit's product segment only; typed // modular clients append the SDK's own client-info after it. - expect(capturedUserAgents[0]).toMatch( - /^@databricks\/appkit\/\d+\.\d+\.\d+/, - ); + expect(capturedUserAgents[0]).toMatch(/^databricks-appkit\/\d+\.\d+\.\d+/); }); test("raw-fetch upload path stamps the AppKit User-Agent on the wire", async () => { @@ -84,9 +82,7 @@ describe("User-Agent attribution (wire)", () => { ); expect(capturedUserAgents).toHaveLength(1); - expect(capturedUserAgents[0]).toMatch( - /^@databricks\/appkit\/\d+\.\d+\.\d+/, - ); + expect(capturedUserAgents[0]).toMatch(/^databricks-appkit\/\d+\.\d+\.\d+/); }); test("dev mode adds the mode/dev User-Agent segment", async () => { diff --git a/packages/shared/src/workspace-client/modular.ts b/packages/shared/src/workspace-client/modular.ts index fc2c02022..acb4b051b 100644 --- a/packages/shared/src/workspace-client/modular.ts +++ b/packages/shared/src/workspace-client/modular.ts @@ -320,22 +320,18 @@ function mapToClientOptions(opts: WorkspaceClientOptions): ClientOptions { } /** - * Wrap the SDK's default fetch transport to prepend AppKit's product segment to - * the outgoing `User-Agent`, preserving the exact legacy string (e.g. - * `@databricks/appkit/0.75.1`) that Databricks-side dashboards match on. + * Wrap the SDK's default fetch transport to prepend AppKit's product segment + * (e.g. `databricks-appkit/0.88.1`) to the outgoing `User-Agent`, which + * Databricks-side dashboards match on. * - * Why the transport and not `setProduct`: the modular SDK's client-info API - * validates the product as a simple token and rejects `@databricks/appkit` (the - * `@`/`/`), and it is process-global. Setting the header on the `httpClient` - * instead keeps the literal product name, is per-client, and — unlike a pnpm - * patch — ships inside appkit's bundled `dist`, so it also reaches deployed apps - * (npm-installed from the tarball, where pnpm patches do not apply). The SDK's - * own client-info (`sdk-js-core/…`, runtime) is already on `request.headers`, so - * prepending keeps it intact after AppKit's segment. + * Why the transport and not the SDK's `setProduct`: that is process-global, + * while the header on the `httpClient` is per-client, and the wrapper ships + * inside appkit's bundled `dist`, so it also reaches deployed apps. The SDK's + * own client-info (`sdk-js-core/…`, runtime) is already on `request.headers`, + * so prepending keeps it intact after AppKit's segment. * - * Returns `undefined` when no product is configured (build-time callers), leaving - * the SDK's default User-Agent untouched — matching the legacy behavior where - * build-time clients carried no AppKit UA. + * The wrapper also retries (see `sendWithRetry`). With no product configured + * (build-time callers) it adds no AppKit segment, like the legacy behavior. */ function buildHttpClient(opts: WorkspaceClientOptions): HttpClient { const co = opts.clientOptions; diff --git a/packages/shared/src/workspace-client/tests/modular.test.ts b/packages/shared/src/workspace-client/tests/modular.test.ts index 595b774e5..ef5ee12c1 100644 --- a/packages/shared/src/workspace-client/tests/modular.test.ts +++ b/packages/shared/src/workspace-client/tests/modular.test.ts @@ -336,31 +336,31 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { ); }); - test("User-Agent: prepends the exact @databricks/appkit product segment (dashboards match on it)", async () => { + test("User-Agent: prepends the exact databricks-appkit product segment (dashboards match on it)", async () => { // Regression: the modular SDK's `setProduct` rejects `@databricks/appkit` // (the `@`/`/`). We set the UA on the httpClient transport instead, keeping // the literal legacy product string that Databricks-side dashboards match. buildWarehousesClient({ clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", userAgentExtra: { mode: "dev" }, }, } as never); // The SDK's own client-info UA is already on the request; ours prepends. const ua = await sentUserAgent(ctorOpts[0].httpClient, "sdk-js-core/1.0.0"); - expect(ua).toBe("@databricks/appkit/0.64.0 mode/dev sdk-js-core/1.0.0"); + expect(ua).toBe("databricks-appkit/0.64.0 mode/dev sdk-js-core/1.0.0"); }); test("User-Agent: sets the product segment even when the request has no prior UA", async () => { buildWarehousesClient({ clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", }, } as never); const ua = await sentUserAgent(ctorOpts[0].httpClient); - expect(ua).toBe("@databricks/appkit/0.64.0"); + expect(ua).toBe("databricks-appkit/0.64.0"); }); test("genie (asUser) uses the OBO token as PAT and keeps the AppKit User-Agent", async () => { @@ -370,7 +370,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { token: "user-token", host: "https://x", clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", }, } as never).genieGetConversationMessage({} as never); @@ -380,7 +380,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { }); expect(m2mOpts).toEqual([]); expect(await sentUserAgent(ctorOpts[0].httpClient)).toBe( - "@databricks/appkit/0.64.0", + "databricks-appkit/0.64.0", ); }); @@ -406,7 +406,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { host: "x.cloud.databricks.com", token: "user-token", clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", }, } as never).getApp({} as never); @@ -417,7 +417,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { }); expect(m2mOpts).toEqual([]); expect(await sentUserAgent(ctorOpts[0].httpClient)).toBe( - "@databricks/appkit/0.64.0", + "databricks-appkit/0.64.0", ); }); @@ -442,7 +442,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { token: "user-token", host: "https://x", clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", }, } as never).getDatabaseInstance({} as never); @@ -452,7 +452,7 @@ describe("modular mapToClientOptions (via buildWarehousesClient)", () => { }); expect(m2mOpts).toEqual([]); expect(await sentUserAgent(ctorOpts[0].httpClient)).toBe( - "@databricks/appkit/0.64.0", + "databricks-appkit/0.64.0", ); }); @@ -778,7 +778,7 @@ describe("buildWorkspaceAuth (auth + raw-request seam)", () => { host: "ws.cloud.databricks.com", token: "t", clientOptions: { - product: "@databricks/appkit", + product: "databricks-appkit", productVersion: "0.64.0", }, } as never); @@ -794,7 +794,7 @@ describe("buildWorkspaceAuth (auth + raw-request seam)", () => { "https://ws.cloud.databricks.com/api/2.0/sql/warehouses?skip_cannot_use=true", ); expect(sent[0].method).toBe("GET"); - expect(sent[0].headers.get("User-Agent")).toBe("@databricks/appkit/0.64.0"); + expect(sent[0].headers.get("User-Agent")).toBe("databricks-appkit/0.64.0"); expect(sent[0].headers.get("Authorization")).toBe("Bearer t"); expect(sent[0].headers.get("X-Extra")).toBe("1"); });