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
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions packages/appkit/src/context/client-options.ts
Original file line number Diff line number Diff line change
@@ -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/<version>`), so outbound Databricks traffic
* User-Agent (`databricks-appkit/<version>`), so outbound Databricks traffic
* is attributable to AppKit. Use this for every `WorkspaceClient` AppKit
* constructs at runtime.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
24 changes: 10 additions & 14 deletions packages/shared/src/workspace-client/modular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 13 additions & 13 deletions packages/shared/src/workspace-client/tests/modular.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);
Expand All @@ -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",
);
});

Expand All @@ -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);
Expand All @@ -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",
);
});

Expand All @@ -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);
Expand All @@ -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",
);
});

Expand Down Expand Up @@ -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);
Expand All @@ -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");
});
Expand Down
Loading