Skip to content
Snippets Groups Projects
Unverified Commit ceba6e40 authored by Nicolò Pretto's avatar Nicolò Pretto Committed by GitHub
Browse files

feat(sdk): detect mismatch between sdk version and mb version (#50032)


* feat(sdk): detect mismatch between sdk version and mb version

* refactor: log utils to print the big headers

* Update enterprise/frontend/src/embedding-sdk/lib/version-utils.unit.spec.ts

Co-authored-by: default avatarMahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>

* refactor: call -> callArguments

* add link to the docs

---------

Co-authored-by: default avatarMahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
parent 077236f2
No related branches found
No related tags found
No related merge requests found
......@@ -4,5 +4,5 @@ export const EMBEDDING_SDK_PORTAL_ROOT_ELEMENT_ID = "metabase-sdk-portal-root";
export const EMBEDDING_SDK_FULL_PAGE_PORTAL_ROOT_ELEMENT_ID =
"metabase-sdk-full-page-portal-root";
export const getEmbeddingSdkVersion = () =>
process.env.EMBEDDING_SDK_VERSION ?? "unknown";
export const getEmbeddingSdkVersion = (): string | "unknown" =>
(process.env.EMBEDDING_SDK_VERSION as string) ?? "unknown";
// Note: this functions need to return an array of two elements, when styling
// console.logs, the style needs to passed as second argument
// To use them, do `console.log(...bigWarningHeader("message"), "rest of the message")`
// Note 2: why do we even need those? Because at the moment the SDK may log a
// lot of exceptions, we need to make actionable messages stand out
export const bigWarningHeader = (message: string) => {
return [
`%c${message}\n`,
"color: #FCF0A6; font-size: 16px; font-weight: bold;",
];
};
export const bigErrorHeader = (message: string) => {
return [
`%c${message}\n`,
"color: #FF2222; font-size: 16px; font-weight: bold;",
];
};
import { versionToNumericComponents } from "metabase/lib/utils";
export const isSdkVersionCompatibleWithMetabaseVersion = ({
mbVersion,
sdkVersion,
}: {
mbVersion: string;
sdkVersion: string;
}) => {
const mbVersionComponents = versionToNumericComponents(mbVersion);
const sdkVersionComponents = versionToNumericComponents(sdkVersion);
return mbVersionComponents?.[1] === sdkVersionComponents?.[1];
};
import { isSdkVersionCompatibleWithMetabaseVersion } from "./version-utils";
const expectCompatibility = ({
mbVersion,
sdkVersion,
expected,
}: {
mbVersion: string;
sdkVersion: string;
expected: boolean;
}) => {
it(`expect sdk version ${sdkVersion} and mb version ${mbVersion} to be ${expected ? "compatible" : "incompatible"}`, () => {
expect(
isSdkVersionCompatibleWithMetabaseVersion({
mbVersion,
sdkVersion,
}),
).toBe(expected);
});
};
describe('sdk version utils, naming used: "{0,1}.{major}.{minor}"', () => {
describe('should return true only if the "major" version is the same', () => {
expectCompatibility({
mbVersion: "v0.52.10",
sdkVersion: "0.52.10",
expected: true,
});
expectCompatibility({
mbVersion: "v1.50.10",
sdkVersion: "0.51.10",
expected: false,
});
expectCompatibility({
mbVersion: "v1.50.10",
sdkVersion: "0.53.10",
expected: false,
});
});
describe('should ignore "minors"', () => {
expectCompatibility({
mbVersion: "v1.50.10",
sdkVersion: "0.50.11",
expected: true,
});
expectCompatibility({
mbVersion: "v1.50.10",
sdkVersion: "0.50.9",
expected: true,
});
});
describe("sdk version 0.xx.yy should be compatible both with MB 0.xx.yy (OSS) and 1.xx.yy (EE)", () => {
expectCompatibility({
mbVersion: "v0.52.10",
sdkVersion: "0.52.10",
expected: true,
});
expectCompatibility({
mbVersion: "v1.52.10",
sdkVersion: "0.52.10",
expected: true,
});
});
describe("should ignore build tags like snapshot, alpha, beta, rc", () => {
for (const tag of ["snapshot", "alpha", "beta", "rc", "X-NOT-EXISTING"]) {
expectCompatibility({
mbVersion: `v0.52.10-${tag}`,
sdkVersion: "0.52.10",
expected: true,
});
}
});
describe("should handle versions of the sdk wrapped in double quotes (metabase#50014)", () => {
expectCompatibility({
mbVersion: "v1.55.0",
sdkVersion: '"0.55.0"',
expected: true,
});
expectCompatibility({
mbVersion: "v1.55.0",
sdkVersion: '"0.54.0"',
expected: false,
});
});
});
......@@ -3,7 +3,10 @@ import type {
FetchRequestTokenFn,
SDKConfig,
} from "embedding-sdk";
import { getEmbeddingSdkVersion } from "embedding-sdk/config";
import { getIsLocalhost } from "embedding-sdk/lib/is-localhost";
import { bigErrorHeader, bigWarningHeader } from "embedding-sdk/lib/log-utils";
import { isSdkVersionCompatibleWithMetabaseVersion } from "embedding-sdk/lib/version-utils";
import type { SdkStoreState } from "embedding-sdk/store/types";
import api from "metabase/lib/api";
import { createAsyncThunk } from "metabase/lib/redux";
......@@ -43,6 +46,25 @@ export const initAuth = createAsyncThunk(
dispatch(refreshSiteSettings({})),
]);
const mbVersion = siteSettings.payload?.version?.tag;
const sdkVersion = getEmbeddingSdkVersion();
if (mbVersion && sdkVersion !== "unknown") {
if (
!isSdkVersionCompatibleWithMetabaseVersion({
mbVersion,
sdkVersion,
})
) {
console.warn(
...bigWarningHeader("Detected SDK compatibility issue"),
`SDK version ${sdkVersion} is not compatible with MB version ${mbVersion}, this might cause issues.`,
// eslint-disable-next-line no-unconditional-metabase-links-render -- console log in case of issues
"Learn more at https://www.metabase.com/docs/latest/embedding/sdk/version",
);
}
}
if (!user.payload) {
// The refresh user thunk just returns null if it fails to fetch the user, it doesn't throw
const error = new Error(
......@@ -113,11 +135,7 @@ export const refreshTokenAsync = createAsyncThunk(
// The host app may have a lot of logs (and the sdk logs a lot too), so we
// make a big red error message to make it visible as this is 90% a blocking error
console.error(
"%cFailed to get auth session\n",
"color: #FF2222; font-size: 16px; font-weight: bold;",
exception,
);
console.error(...bigErrorHeader("Failed to get auth session"), exception);
throw exception;
}
......
import { render } from "@testing-library/react";
import fetchMock from "fetch-mock";
import {
setupCurrentUserEndpoint,
setupPropertiesEndpoints,
} from "__support__/server-mocks";
import { waitForLoaderToBeRemoved } from "__support__/ui";
import {
MetabaseProvider,
defineEmbeddingSdkConfig,
} from "embedding-sdk/components/public";
import {
createMockSettings,
createMockTokenFeatures,
createMockUser,
createMockVersion,
} from "metabase-types/api/mocks";
import { getEmbeddingSdkVersion } from "../config";
// TODO: extract this common setup to a shared util
const METABASE_INSTANCE_URL = "path:";
const AUTH_PROVIDER_URL = "http://auth-provider/metabase-sso";
const defaultAuthUriConfig = defineEmbeddingSdkConfig({
metabaseInstanceUrl: METABASE_INSTANCE_URL,
authProviderUri: AUTH_PROVIDER_URL,
fetchRequestToken: _ =>
Promise.resolve({
id: "123",
exp: Number.MAX_SAFE_INTEGER,
}),
});
jest.mock("../config", () => ({
getEmbeddingSdkVersion: jest.fn(),
}));
const setup = async ({
sdkVersion,
mbVersion,
}: {
sdkVersion: string;
mbVersion: string;
}) => {
(getEmbeddingSdkVersion as jest.Mock).mockReturnValue(sdkVersion);
setupPropertiesEndpoints(
createMockSettings({
"token-features": createMockTokenFeatures({
embedding_sdk: true,
}),
version: createMockVersion({ tag: mbVersion }),
}),
);
setupCurrentUserEndpoint(createMockUser({ id: 1 }));
render(
<MetabaseProvider config={defaultAuthUriConfig}>
<div>Hello</div>
</MetabaseProvider>,
);
await waitForLoaderToBeRemoved();
};
let consoleWarnSpy: jest.SpyInstance;
const getWarnMessages = (): string[] =>
consoleWarnSpy.mock.calls.map(callArguments => callArguments.join(" "));
describe("SDK auth errors", () => {
beforeEach(() => {
fetchMock.reset();
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
(getEmbeddingSdkVersion as jest.Mock).mockClear();
});
afterEach(() => {
jest.resetAllMocks();
});
describe("SDK version compatibility", () => {
it("should show a message when the SDK version is not compatible with the Metabase version", async () => {
await setup({ sdkVersion: "0.52.10", mbVersion: "v1.55.0" });
expect(
getWarnMessages().filter(message =>
message.includes(
"SDK version 0.52.10 is not compatible with MB version v1.55.0, this might cause issues.",
),
),
).toHaveLength(1);
});
it("should not show a warning when the SDK version is compatible with the Metabase version", async () => {
await setup({ sdkVersion: "0.55.10", mbVersion: "v1.55.1" });
expect(
getWarnMessages().filter(message =>
message.includes("is not compatible"),
),
).toHaveLength(0);
});
it("should not show the warning when the sdk version is unknown", async () => {
await setup({ sdkVersion: "unknown", mbVersion: "v1.55.1" });
expect(
getWarnMessages().filter(message =>
message.includes("is not compatible"),
),
).toHaveLength(0);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment