Skip to content

Commit

Permalink
add iframe dashcards tracking (metabase#48382)
Browse files Browse the repository at this point in the history
* track iframe creation

* Update Loki Snapshots

* Update frontend/src/metabase/visualizations/visualizations/IFrameViz/utils.unit.spec.ts

Co-authored-by: Anton Kulyk <kuliks.anton@gmail.com>

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Anton Kulyk <kuliks.anton@gmail.com>
  • Loading branch information
3 people authored Oct 8, 2024
1 parent 5fd75e0 commit 291384d
Show file tree
Hide file tree
Showing 22 changed files with 262 additions and 17 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_viz_BarChart_Default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .loki/reference/chrome_laptop_viz_SmartScalar_Default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 23 additions & 12 deletions e2e/test/scenarios/dashboard/dashboard.cy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
collectionOnTheGoModal,
commandPalette,
commandPaletteButton,
createDashboard,
createDashboardWithTabs,
createQuestionAndDashboard,
dashboardHeader,
Expand Down Expand Up @@ -326,18 +327,6 @@ describe("scenarios > dashboard", () => {
});

describe("iframe cards", () => {
it("should be possible to add an iframe card", () => {
editDashboard();
addIFrameWhileEditing("https://example.com");
cy.findByTestId("dashboardcard-actions-panel").should("not.exist");
cy.button("Done").click();
getDashboardCards().eq(0).realHover();
cy.findByTestId("dashboardcard-actions-panel").should("be.visible");
validateIFrame("https://example.com");
saveDashboard();
validateIFrame("https://example.com");
});

it("should handle various iframe and URL inputs", () => {
const testCases = [
{
Expand Down Expand Up @@ -1151,6 +1140,28 @@ describeWithSnowplow("scenarios > dashboard", () => {
expectNoBadSnowplowEvents();
});

it("should be possible to add an iframe card", () => {
createDashboard({ name: "iframe card" }).then(({ body: { id } }) => {
visitDashboard(id);

editDashboard();
addIFrameWhileEditing("https://example.com");
cy.findByTestId("dashboardcard-actions-panel").should("not.exist");
cy.button("Done").click();
getDashboardCards().eq(0).realHover();
cy.findByTestId("dashboardcard-actions-panel").should("be.visible");
validateIFrame("https://example.com");
saveDashboard();
validateIFrame("https://example.com");

expectGoodSnowplowEvent({
event: "new_iframe_card_created",
dashboard_id: id,
domain_name: "example.com",
});
});
});

it("saving a dashboard should track a 'dashboard_saved' snowplow event", () => {
visitDashboard(ORDERS_DASHBOARD_ID);
editDashboard();
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/metabase-types/analytics/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type DashboardEventSchema = {
section_layout?: string | null;
full_width?: boolean | null;
dashboard_accessed_via?: string | null;
domain_name?: string | null;
};

type ValidateEvent<
Expand Down Expand Up @@ -72,6 +73,12 @@ export type NewActionCardCreatedEvent = ValidateEvent<{
dashboard_id: number;
}>;

export type NewIFrameCardCreatedEvent = ValidateEvent<{
event: "new_iframe_card_created";
dashboard_id: number;
domain_name: string | null;
}>;

export type CardSetToHideWhenNoResultsEvent = ValidateEvent<{
event: "card_set_to_hide_when_no_results";
dashboard_id: number;
Expand Down Expand Up @@ -132,6 +139,7 @@ export type DashboardEvent =
| NewHeadingCardCreatedEvent
| NewLinkCardCreatedEvent
| NewActionCardCreatedEvent
| NewIFrameCardCreatedEvent
| CardSetToHideWhenNoResultsEvent
| DashboardPdfExportedEvent
| CardMovedToTabEvent
Expand Down
1 change: 1 addition & 0 deletions frontend/src/metabase-types/api/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export type DashboardCardLayoutAttrs = {
export type DashCardVisualizationSettings = {
[key: string]: unknown;
virtual_card?: VirtualCard;
iframe?: string;
};

export type BaseDashboardCard = DashboardCardLayoutAttrs & {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/metabase/dashboard/actions/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { trackDashboardSaved } from "../analytics";
import { getDashboardBeforeEditing } from "../selectors";

import { setEditingDashboard } from "./core";
import { hasDashboardChanged, haveDashboardCardsChanged } from "./utils";
import {
hasDashboardChanged,
haveDashboardCardsChanged,
trackAddedIFrameDashcards,
} from "./utils";

export const UPDATE_DASHBOARD_AND_CARDS =
"metabase/dashboard/UPDATE_DASHBOARD_AND_CARDS";
Expand Down Expand Up @@ -96,6 +100,8 @@ export const updateDashboardAndCards = createThunkAction(
.map(async dc => CardApi.update(dc.card)),
);

trackAddedIFrameDashcards(dashboard);

const dashcardsToUpdate = dashboard.dashcards
.filter(dc => !dc.isRemoved)
.map(dc => ({
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/metabase/dashboard/actions/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { t } from "ttag";
import _ from "underscore";

import { getIframeDomainName } from "metabase/visualizations/visualizations/IFrameViz/utils";
import type {
DashCardId,
Dashboard,
Expand All @@ -10,7 +11,8 @@ import type {
} from "metabase-types/api";
import type { StoreDashboard, StoreDashcard } from "metabase-types/store";

import { isActionDashCard } from "../utils";
import { trackIFrameDashcardsSaved } from "../analytics";
import { isActionDashCard, isIFrameDashCard } from "../utils";

export function getExistingDashCards(
dashboards: Record<DashboardId, StoreDashboard>,
Expand Down Expand Up @@ -83,3 +85,21 @@ export const getDashCardMoveToTabUndoMessage = (dashCard: StoreDashcard) => {
return t`Card moved`;
}
};

export const trackAddedIFrameDashcards = (dashboard: Dashboard) => {
try {
const newIFrameDashcards = dashboard.dashcards.filter(
dashcard =>
"isAdded" in dashcard && dashcard.isAdded && isIFrameDashCard(dashcard),
);

newIFrameDashcards.forEach(dashcard => {
const domainName = getIframeDomainName(
dashcard.visualization_settings?.iframe,
);
trackIFrameDashcardsSaved(dashboard.id, domainName);
});
} catch {
console.error("Could not track added iframe dashcards", dashboard);
}
};
11 changes: 11 additions & 0 deletions frontend/src/metabase/dashboard/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ export const trackCardCreated = (type: CardTypes, dashboardId: DashboardId) => {
}
};

export const trackIFrameDashcardsSaved = (
dashboardId: DashboardId,
domainName: string | null,
) => {
trackSchemaEvent("dashboard", {
event: "new_iframe_card_created",
dashboard_id: getDashboardId(dashboardId),
domain_name: domainName,
});
};

export const trackSectionAdded = (
dashboardId: DashboardId,
sectionId: SectionId,
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/metabase/dashboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ export function isLinkDashCard(
return getVirtualCardType(dashcard) === "link";
}

export function isIFrameDashCard(
dashcard: BaseDashboardCard,
): dashcard is VirtualDashboardCard {
return getVirtualCardType(dashcard) === "iframe";
}

export function isNativeDashCard(dashcard: QuestionDashboardCard) {
// The `dataset_query` is null for questions on a dashboard the user doesn't have access to
return dashcard.card.dataset_query?.type === "native";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/metabase/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const VERSIONS: Record<SchemaType, SchemaVersion> = {
browse_data: "1-0-0",
cleanup: "1-0-0",
csvupload: "1-0-3",
dashboard: "1-1-5",
dashboard: "1-1-6",
database: "1-0-1",
downloads: "1-0-0",
embed_flow: "1-0-2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ const normalizeUrl = (trimmedUrl: string) => {
return trimmedUrl;
};

const isIframeString = (iframeOrUrl: string) =>
iframeOrUrl.startsWith("<iframe");

export const getIframeDomainName = (
iframeOrUrl: string | undefined,
): string | null => {
if (!iframeOrUrl) {
return null;
}
const trimmedInput = iframeOrUrl.trim();

try {
const url = isIframeString(trimmedInput)
? parseUrlFromIframe(trimmedInput)
: normalizeUrl(trimmedInput);

if (!url) {
return null;
}

const urlObject = new URL(url);
return urlObject.hostname;
} catch {
return null;
}
};

export const getIframeUrl = (
iframeOrUrl: string | undefined,
): string | null => {
Expand All @@ -104,7 +131,7 @@ export const getIframeUrl = (

const trimmedInput = iframeOrUrl.trim();

if (trimmedInput.startsWith("<iframe")) {
if (isIframeString(trimmedInput)) {
return parseUrlFromIframe(trimmedInput);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getIframeUrl } from "./utils";
import { getIframeDomainName, getIframeUrl } from "./utils";

describe("getIframeUrl", () => {
describe("share to embed link transformation", () => {
Expand Down Expand Up @@ -122,3 +122,34 @@ describe("getIframeUrl", () => {
expect(result).toBeNull();
});
});

describe("getIframeDomainName", () => {
it("should return the domain name for a valid URL", () => {
const result = getIframeDomainName("https://example.com/path/to/page");
expect(result).toBe("example.com");
});

it("should return the domain name for a URL without protocol", () => {
const result = getIframeDomainName("www.example.com");
expect(result).toBe("www.example.com");
});

it("should return null for invalid URLs", () => {
expect(getIframeDomainName("not a url")).toBeNull();
expect(getIframeDomainName("https://example.com:asdf")).toBeNull();
expect(getIframeDomainName("")).toBeNull();
expect(getIframeDomainName(undefined)).toBeNull();
});

it("should extract domain name from iframe src", () => {
const input = '<iframe src="https://example.com/embed"></iframe>';
const result = getIframeDomainName(input);
expect(result).toBe("example.com");
});

it("should return null for iframe without src", () => {
const input = "<iframe></iframe>";
const result = getIframeDomainName(input);
expect(result).toBeNull();
});
});
Loading

0 comments on commit 291384d

Please sign in to comment.