From d43863a2c10b6831bf9dbb50863e16aa4eb89c97 Mon Sep 17 00:00:00 2001
From: Oisin Coveney <oisin@metabase.com>
Date: Fri, 9 Feb 2024 18:57:46 +0100
Subject: [PATCH] Add snowplow events for embedding setup flow (#37617)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Add snowplow events for embedding setup flow

* Fix dashboard model tests

* Fix dashboard tests

* Fix card tests

* Fix dashboard API tests

* Fix type error

* Implement analytics 2/7

1. public_link_copied
2. public_link_removed

* Fix snowplow schema enum

Removing null as we don't seem to put `null` in `enum`

* Complete the embed_flow snowplow schema

* Modify `initial_published_at` to not be overridden

* Implement analytics 4/7

1. public_link_copied
2. public_link_removed
3. public_embed_code_copied
4. static_embed_published
5. static_embed_unpublished

* Prevent accidental ESLint log in Cypress

* Differentiate `EmbeddingParametersSettings` from `EmbeddingParametersValues`

We were mixing the type before, as the former one are the type of the
setting values, not the actual values for the parameters.

* Fix dashboard and card types

* Fix `params` count for `static_embed_published` event

* fixup! Prevent accidental ESLint log in Cypress

* Fix ESlint from the result of upgrading ESLint package

* Fix Snowplow schema enum to include null

* Fully implement `static_embed_code_copied`

* Finish embed_flow snowplow 🎉

* Fix unit tests because markup change

* Fix E2E tests

* Fix E2E CI failure

Apparently using `.realClick()` on copy button could fail locally,
but pass on CI and vice-versa. I couldn't replicate this all the time.

I'm not sure why it would fail locally.

* Fix copy to clipboard not working on CI

* Improve test readability

* Remove extra test-id

* Fix refactor `*CodeOptionId`

* Restrict Snowplow schema

* Revert unnecessary change

* Apply Cal's suggestions for BE improvements

Co-authored-by: Cal Herries <39073188+calherries@users.noreply.github.com>

* Update src/metabase/util/embed.clj

Co-authored-by: Cal Herries <39073188+calherries@users.noreply.github.com>

* Fix backend linter error, hopefully.

---------

Co-authored-by: Mahatthana Nomsawadi <mahatthana.n@gmail.com>
Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
Co-authored-by: Cal Herries <39073188+calherries@users.noreply.github.com>
Co-authored-by: Nicolò Pretto <info@npretto.com>
---
 e2e/support/helpers/e2e-snowplow-helpers.js   |  49 +-
 .../helpers/e2e-ui-elements-helpers.js        |   5 +
 .../embedding-downloads-questions.cy.spec.js  |   2 +-
 .../embedding/embedding-smoketests.cy.spec.js |   4 +-
 ...c-sharing-embed-button-behavior.cy.spec.js | 556 +++++++++++++++++-
 frontend/src/metabase-types/api/card.ts       |   3 +
 frontend/src/metabase-types/api/dashboard.ts  |   5 +-
 frontend/src/metabase-types/api/mocks/card.ts |   3 +
 .../src/metabase-types/api/mocks/dashboard.ts |   3 +
 .../QueryActionContextProvider.tsx            |   3 +
 .../src/metabase/dashboard/actions/sharing.ts |   5 +-
 .../components/DashboardTabs/test-utils.ts    |   3 +
 .../DashboardPublicLinkPopover.tsx            |  15 +
 .../PublicLinkPopover/PublicLinkCopyPanel.tsx |   5 +-
 .../PublicLinkPopover/PublicLinkPopover.tsx   |   3 +
 .../QuestionPublicLinkPopover.tsx             |  16 +
 frontend/src/metabase/dashboard/reducers.js   |  10 +-
 .../SelectEmbedTypePane.tsx                   |  17 +
 .../SharingPaneButton/SharingPaneButton.tsx   |   8 +-
 .../AppearanceSettings.tsx                    |  38 +-
 .../ClientEmbedCodePane.tsx                   |  19 +-
 .../StaticEmbedSetupPane/CodeSample.tsx       |  16 +-
 .../EmbedModalContentStatusBar.tsx            |   8 +-
 .../StaticEmbedSetupPane/OverviewSettings.tsx |  16 +-
 .../ServerEmbedCodePane.tsx                   |  50 +-
 .../StaticEmbedSetupPane.tsx                  | 170 ++++--
 .../EmbedModal/StaticEmbedSetupPane/config.ts |  17 +-
 .../EmbedModal/StaticEmbedSetupPane/tabs.ts   |   5 +
 .../tests/common.unit.spec.tsx                |   6 +-
 .../tests/premium.unit.spec.tsx               |  12 +-
 frontend/src/metabase/public/lib/analytics.ts | 154 +++++
 .../src/metabase/public/lib/code-templates.ts |  45 +-
 frontend/src/metabase/public/lib/code.ts      |  16 +-
 frontend/src/metabase/public/lib/embed.ts     |  10 +-
 frontend/src/metabase/public/lib/types.ts     |   8 +-
 .../metabase/query_builder/actions/sharing.ts |   5 +-
 .../src/metabase/query_builder/reducers.js    |   1 +
 .../ui/components/overlays/Popover/index.ts   |   3 -
 .../ui/components/overlays/Popover/index.tsx  |  18 +
 .../migrations/001_update_migrations.yaml     |  32 +
 .../com.metabase/embed_flow/jsonschema/1-0-0  | 198 +++++++
 src/metabase/models/card.clj                  |   4 +-
 src/metabase/models/dashboard.clj             |   8 +-
 src/metabase/util/embed.clj                   |   7 +
 test/metabase/api/card_test.clj               |  45 +-
 test/metabase/api/dashboard_test.clj          |   1 +
 test/metabase/util/embed_test.clj             |  29 +-
 47 files changed, 1465 insertions(+), 191 deletions(-)
 create mode 100644 frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tabs.ts
 create mode 100644 frontend/src/metabase/public/lib/analytics.ts
 delete mode 100644 frontend/src/metabase/ui/components/overlays/Popover/index.ts
 create mode 100644 frontend/src/metabase/ui/components/overlays/Popover/index.tsx
 create mode 100644 snowplow/iglu-client-embedded/schemas/com.metabase/embed_flow/jsonschema/1-0-0

diff --git a/e2e/support/helpers/e2e-snowplow-helpers.js b/e2e/support/helpers/e2e-snowplow-helpers.js
index a4cc0f50c8c..b44c957a459 100644
--- a/e2e/support/helpers/e2e-snowplow-helpers.js
+++ b/e2e/support/helpers/e2e-snowplow-helpers.js
@@ -29,11 +29,58 @@ export const expectGoodSnowplowEvent = (eventData, count = 1) => {
     "micro/good",
     ({ body }) =>
       body.filter(snowplowEvent =>
-        _.isMatch(snowplowEvent?.event?.unstruct_event?.data?.data, eventData),
+        isDeepMatch(
+          snowplowEvent?.event?.unstruct_event?.data?.data,
+          eventData,
+        ),
       ).length === count,
   ).should("be.ok");
 };
 
+export function isDeepMatch(objectOrValue, partialObjectOrValue) {
+  if (isMatcher(partialObjectOrValue)) {
+    return partialObjectOrValue(objectOrValue);
+  }
+
+  const bothAreNotObjects =
+    // Check null because typeof null === "object"
+    objectOrValue == null ||
+    partialObjectOrValue == null ||
+    typeof objectOrValue !== "object" ||
+    typeof partialObjectOrValue !== "object";
+
+  // Exit condition when calling recursively
+  if (bothAreNotObjects) {
+    return objectOrValue === partialObjectOrValue;
+  }
+
+  for (const [key, value] of Object.entries(partialObjectOrValue)) {
+    if (Array.isArray(value)) {
+      if (!isArrayDeepMatch(objectOrValue[key], value)) {
+        return false;
+      }
+    } else if (!isDeepMatch(objectOrValue[key], value)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+function isMatcher(value) {
+  return typeof value === "function";
+}
+
+function isArrayDeepMatch(array, partialArray) {
+  for (const index in partialArray) {
+    if (!isDeepMatch(array[index], partialArray[index])) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
 export const expectGoodSnowplowEvents = count => {
   retrySnowplowRequest("micro/good", ({ body }) => body.length >= count)
     .its("body")
diff --git a/e2e/support/helpers/e2e-ui-elements-helpers.js b/e2e/support/helpers/e2e-ui-elements-helpers.js
index a3472ff58f5..6c1b497ba7b 100644
--- a/e2e/support/helpers/e2e-ui-elements-helpers.js
+++ b/e2e/support/helpers/e2e-ui-elements-helpers.js
@@ -7,6 +7,11 @@ export function popover() {
   return cy.get(POPOVER_ELEMENT);
 }
 
+export function mantinePopover() {
+  const MANTINE_POPOVER = "[data-popover=mantine-popover]";
+  return cy.get(MANTINE_POPOVER).should("be.visible");
+}
+
 const HOVERCARD_ELEMENT = ".emotion-HoverCard-dropdown[role='dialog']";
 
 export function hovercard() {
diff --git a/e2e/test/scenarios/embedding/embedding-downloads-questions.cy.spec.js b/e2e/test/scenarios/embedding/embedding-downloads-questions.cy.spec.js
index 7d5f40e7415..9c8aed32747 100644
--- a/e2e/test/scenarios/embedding/embedding-downloads-questions.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-downloads-questions.cy.spec.js
@@ -109,7 +109,7 @@ describeEE("scenarios > embedding > questions > downloads", () => {
         });
 
         cy.log("Disable downloads");
-        cy.findByLabelText("Enable users to download data from this embed")
+        cy.findByLabelText("Download data")
           .as("allow-download-toggle")
           .should("be.checked");
 
diff --git a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
index dd039426a8c..f9d82516afc 100644
--- a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
@@ -155,7 +155,9 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
           cy.findByRole("tab", { name: "Appearance" }).click();
 
           cy.findByText("Background");
-          cy.findByText("Dashboard title");
+          cy.findByText(
+            object === "dashboard" ? "Dashboard title" : "Question title",
+          );
           cy.findByText("Border");
           cy.findByText(
             (_, element) =>
diff --git a/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js b/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js
index 1ad00d2ddac..0e4bce55b93 100644
--- a/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js
+++ b/e2e/test/scenarios/sharing/public-sharing-embed-button-behavior.cy.spec.js
@@ -1,20 +1,26 @@
-import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
 import {
   createPublicDashboardLink,
   createPublicQuestionLink,
   describeEE,
+  describeWithSnowplow,
+  enableTracking,
+  expectGoodSnowplowEvent,
+  expectNoBadSnowplowEvents,
   getEmbedModalSharingPane,
+  mantinePopover,
+  modal,
   openEmbedModalFromMenu,
   openPublicLinkPopoverFromMenu,
+  openStaticEmbeddingModal,
+  popover,
+  resetSnowplow,
   restore,
   setTokenFeatures,
   visitDashboard,
   visitQuestion,
 } from "e2e/support/helpers";
 
-const { PRODUCTS_ID } = SAMPLE_DATABASE;
-
-["dashboard", "card"].forEach(resource => {
+["dashboard", "question"].forEach(resource => {
   describe(`embed modal behavior for ${resource}s`, () => {
     beforeEach(() => {
       restore();
@@ -250,6 +256,469 @@ describe("embed modal display", () => {
   });
 });
 
+["dashboard", "question"].forEach(resource => {
+  describeWithSnowplow(`public ${resource} sharing snowplow events`, () => {
+    beforeEach(() => {
+      restore();
+      resetSnowplow();
+      cy.signInAsAdmin();
+      enableTracking();
+
+      createResource(resource).then(({ body }) => {
+        cy.wrap(body).as("resource");
+        cy.wrap(body.id).as("resourceId");
+      });
+    });
+
+    afterEach(() => {
+      expectNoBadSnowplowEvents();
+    });
+
+    describe(`when embedding ${resource}`, () => {
+      describe("when interacting with public link popover", () => {
+        it("should send `public_link_copied` event when copying public link", () => {
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+
+          openPublicLinkPopoverFromMenu();
+          cy.findByTestId("copy-button").realClick();
+          if (resource === "dashboard") {
+            expectGoodSnowplowEvent({
+              event: "public_link_copied",
+              artifact: "dashboard",
+              format: null,
+            });
+          }
+
+          if (resource === "question") {
+            expectGoodSnowplowEvent({
+              event: "public_link_copied",
+              artifact: "question",
+              format: "html",
+            });
+
+            mantinePopover().findByText("csv").click();
+            cy.findByTestId("copy-button").realClick();
+            expectGoodSnowplowEvent({
+              event: "public_link_copied",
+              artifact: "question",
+              format: "csv",
+            });
+
+            mantinePopover().findByText("xlsx").click();
+            cy.findByTestId("copy-button").realClick();
+            expectGoodSnowplowEvent({
+              event: "public_link_copied",
+              artifact: "question",
+              format: "xlsx",
+            });
+
+            mantinePopover().findByText("json").click();
+            cy.findByTestId("copy-button").realClick();
+            expectGoodSnowplowEvent({
+              event: "public_link_copied",
+              artifact: "question",
+              format: "json",
+            });
+          }
+        });
+
+        it("should send `public_link_removed` when removing the public link", () => {
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+
+          openPublicLinkPopoverFromMenu();
+          mantinePopover().button("Remove public link").click();
+          expectGoodSnowplowEvent({
+            event: "public_link_removed",
+            artifact: resource,
+            source: "public-share",
+          });
+        });
+      });
+
+      describe("when interacting with public embedding", () => {
+        it("should send `public_embed_code_copied` event when copying the public embed iframe", () => {
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+
+          openEmbedModalFromMenu();
+          cy.findByTestId("sharing-pane-public-embed-button").within(() => {
+            cy.findByText("Get an embed link").click();
+            cy.findByTestId("copy-button").realClick();
+          });
+          expectGoodSnowplowEvent({
+            event: "public_embed_code_copied",
+            artifact: resource,
+            source: "public-embed",
+          });
+        });
+
+        it("should send `public_link_removed` event when removing the public embed", () => {
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+
+          openEmbedModalFromMenu();
+          cy.findByTestId("sharing-pane-public-embed-button").within(() => {
+            cy.findByText("Get an embed link").click();
+            cy.button("Remove public URL").click();
+          });
+          expectGoodSnowplowEvent({
+            event: "public_link_removed",
+            artifact: resource,
+            source: "public-embed",
+          });
+        });
+      });
+
+      describe("when interacting with static embedding", () => {
+        it("should send `static_embed_code_copied` when copying the static embed code", () => {
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+          openStaticEmbeddingModal();
+
+          cy.log("Assert copying codes in Overview tab");
+          cy.findByTestId("embed-backend")
+            .findByTestId("copy-button")
+            .realClick();
+          expectGoodSnowplowEvent({
+            event: "static_embed_code_copied",
+            artifact: resource,
+            language: "node",
+            location: "code_overview",
+            code: "backend",
+            appearance: {
+              bordered: true,
+              titled: true,
+              font: "instance",
+              theme: "light",
+              hide_download_button: null,
+            },
+          });
+
+          cy.findByTestId("embed-frontend")
+            .findByTestId("copy-button")
+            .realClick();
+          expectGoodSnowplowEvent({
+            event: "static_embed_code_copied",
+            artifact: resource,
+            language: "pug",
+            location: "code_overview",
+            code: "view",
+            appearance: {
+              bordered: true,
+              titled: true,
+              font: "instance",
+              theme: "light",
+              hide_download_button: null,
+            },
+          });
+
+          cy.log("Assert copying code in Parameters tab");
+          modal().within(() => {
+            cy.findByRole("tab", { name: "Parameters" }).click();
+
+            cy.findByText("Node.js").click();
+          });
+          popover().findByText("Ruby").click();
+          cy.findByTestId("embed-backend")
+            .findByTestId("copy-button")
+            .realClick();
+          expectGoodSnowplowEvent({
+            event: "static_embed_code_copied",
+            artifact: resource,
+            language: "ruby",
+            location: "code_params",
+            code: "backend",
+            appearance: {
+              bordered: true,
+              titled: true,
+              font: "instance",
+              theme: "light",
+              hide_download_button: null,
+            },
+          });
+
+          cy.log("Assert copying code in Appearance tab");
+          modal().within(() => {
+            cy.findByRole("tab", { name: "Appearance" }).click();
+
+            cy.findByText("Ruby").click();
+          });
+
+          popover().findByText("Python").click();
+
+          modal().within(() => {
+            cy.findByLabelText("Dark").click({ force: true });
+            if (resource === "dashboard") {
+              cy.findByLabelText("Dashboard title").click({ force: true });
+            }
+            if (resource === "question") {
+              cy.findByLabelText("Question title").click({ force: true });
+            }
+            cy.findByLabelText("Border").click({ force: true });
+          });
+
+          cy.findByTestId("embed-backend")
+            .findByTestId("copy-button")
+            .realClick();
+          expectGoodSnowplowEvent({
+            event: "static_embed_code_copied",
+            artifact: resource,
+            language: "python",
+            location: "code_appearance",
+            code: "backend",
+            appearance: {
+              bordered: false,
+              titled: false,
+              font: "instance",
+              theme: "night",
+              hide_download_button: null,
+            },
+          });
+        });
+
+        describeEE("Pro/EE instances", () => {
+          beforeEach(() => {
+            setTokenFeatures("all");
+          });
+
+          it("should send `static_embed_code_copied` when copying the static embed code", () => {
+            cy.get("@resourceId").then(id => {
+              visitResource(resource, id);
+            });
+            openStaticEmbeddingModal({ acceptTerms: false });
+
+            cy.log("Assert copying codes in Overview tab");
+            cy.findByTestId("embed-backend")
+              .findByTestId("copy-button")
+              .realClick();
+            expectGoodSnowplowEvent({
+              event: "static_embed_code_copied",
+              artifact: resource,
+              language: "node",
+              location: "code_overview",
+              code: "backend",
+              appearance: {
+                bordered: true,
+                titled: true,
+                font: "instance",
+                theme: "light",
+                hide_download_button: resource === "question" ? false : null,
+              },
+            });
+
+            cy.findByTestId("embed-frontend")
+              .findByTestId("copy-button")
+              .realClick();
+            expectGoodSnowplowEvent({
+              event: "static_embed_code_copied",
+              artifact: resource,
+              language: "pug",
+              location: "code_overview",
+              code: "view",
+              appearance: {
+                bordered: true,
+                titled: true,
+                font: "instance",
+                theme: "light",
+                hide_download_button: resource === "question" ? false : null,
+              },
+            });
+
+            cy.log("Assert copying code in Parameters tab");
+            modal().within(() => {
+              cy.findByRole("tab", { name: "Parameters" }).click();
+
+              cy.findByText("Node.js").click();
+            });
+            popover().findByText("Ruby").click();
+            cy.findByTestId("embed-backend")
+              .findByTestId("copy-button")
+              .realClick();
+            expectGoodSnowplowEvent({
+              event: "static_embed_code_copied",
+              artifact: resource,
+              language: "ruby",
+              location: "code_params",
+              code: "backend",
+              appearance: {
+                bordered: true,
+                titled: true,
+                font: "instance",
+                theme: "light",
+                hide_download_button: resource === "question" ? false : null,
+              },
+            });
+
+            cy.log("Assert copying code in Appearance tab");
+            modal().within(() => {
+              cy.findByRole("tab", { name: "Appearance" }).click();
+
+              cy.findByText("Ruby").click();
+            });
+
+            popover().findByText("Python").click();
+
+            modal().within(() => {
+              cy.findByLabelText("Dark").click({ force: true });
+              if (resource === "dashboard") {
+                cy.findByLabelText("Dashboard title").click({ force: true });
+              }
+              if (resource === "question") {
+                cy.findByLabelText("Question title").click({ force: true });
+              }
+              cy.findByLabelText("Border").click({ force: true });
+              cy.findByLabelText("Font").click();
+            });
+
+            popover().findByText("Oswald").click();
+
+            if (resource === "question") {
+              modal().findByLabelText("Download data").click({ force: true });
+            }
+
+            cy.findByTestId("embed-backend")
+              .findByTestId("copy-button")
+              .realClick();
+            expectGoodSnowplowEvent({
+              event: "static_embed_code_copied",
+              artifact: resource,
+              language: "python",
+              location: "code_appearance",
+              code: "backend",
+              appearance: {
+                bordered: false,
+                titled: false,
+                font: "custom",
+                theme: "night",
+                hide_download_button: resource === "question" ? true : null,
+              },
+            });
+          });
+        });
+
+        it("should send `static_embed_discarded` when discarding changes in the static embed modal", () => {
+          cy.get("@resourceId").then(id => {
+            enableEmbeddingForResource({ resource, id });
+            visitResource(resource, id);
+          });
+
+          cy.log("changing parameters, so we could discard changes");
+          openStaticEmbeddingModal({ activeTab: "parameters" });
+          modal().button("Price").click();
+          popover().findByText("Editable").click();
+
+          cy.findByTestId("embed-modal-content-status-bar").within(() => {
+            cy.findByText("Discard changes").click();
+          });
+
+          expectGoodSnowplowEvent({
+            event: "static_embed_discarded",
+            artifact: resource,
+          });
+        });
+
+        it("should send `static_embed_published` when publishing changes in the static embed modal", () => {
+          cy.then(function () {
+            this.timeAfterResourceCreation = Date.now();
+          });
+          cy.get("@resourceId").then(id => {
+            visitResource(resource, id);
+          });
+          openStaticEmbeddingModal();
+
+          cy.findByTestId("embed-modal-content-status-bar")
+            .button("Publish")
+            .click();
+
+          cy.then(function () {
+            expectGoodSnowplowEvent({
+              event: "static_embed_published",
+              artifact: resource,
+              new_embed: true,
+              time_since_creation: closeTo(
+                toSecond(Date.now() - this.timeAfterResourceCreation),
+                1,
+              ),
+              time_since_initial_publication: null,
+              params: {
+                disabled: 3,
+                locked: 0,
+                enabled: 0,
+              },
+            });
+          });
+
+          cy.log("Assert `time_since_initial_publication` and `params`");
+          cy.findByTestId("embed-modal-content-status-bar")
+            .button("Unpublish")
+            .click();
+
+          modal().findByRole("tab", { name: "Parameters" }).click();
+          modal().button("Price").click();
+          popover().findByText("Editable").click();
+
+          modal().button("Category").click();
+          popover().findByText("Locked").click();
+
+          cy.then(function () {
+            const HOUR = 60 * 60 * 1000;
+            const timeAfterPublication = Date.now() + HOUR;
+            cy.log("Mocks the clock to 1 hour later");
+            cy.clock(new Date(timeAfterPublication));
+            cy.findByTestId("embed-modal-content-status-bar")
+              .button("Publish")
+              .click();
+
+            expectGoodSnowplowEvent({
+              event: "static_embed_published",
+              artifact: resource,
+              new_embed: false,
+              time_since_creation: closeTo(toSecond(HOUR), 10),
+              time_since_initial_publication: closeTo(toSecond(HOUR), 10),
+              params: {
+                disabled: 1,
+                locked: 1,
+                enabled: 1,
+              },
+            });
+          });
+        });
+
+        it("should send `static_embed_unpublished` when unpublishing changes in the static embed modal", () => {
+          cy.get("@resourceId").then(id => {
+            enableEmbeddingForResource({ resource, id });
+            visitResource(resource, id);
+          });
+          openStaticEmbeddingModal();
+
+          const HOUR = 60 * 60 * 1000;
+          cy.clock(new Date(Date.now() + HOUR));
+          cy.findByTestId("embed-modal-content-status-bar").within(() => {
+            cy.findByText("Unpublish").click();
+          });
+
+          expectGoodSnowplowEvent({
+            event: "static_embed_unpublished",
+            artifact: resource,
+            time_since_creation: closeTo(toSecond(HOUR), 10),
+            time_since_initial_publication: closeTo(toSecond(HOUR), 10),
+          });
+        });
+      });
+    });
+  });
+});
+
+function toSecond(milliseconds) {
+  return Math.round(milliseconds / 1000);
+}
 function expectDisabledButtonWithTooltipLabel(tooltipLabel) {
   cy.findByTestId("dashboard-embed-button").should("be.disabled");
   cy.findByTestId("dashboard-embed-button").realHover();
@@ -257,21 +726,73 @@ function expectDisabledButtonWithTooltipLabel(tooltipLabel) {
 }
 
 function createResource(resource) {
-  if (resource === "card") {
-    return cy.createQuestion({
+  if (resource === "question") {
+    return cy.createNativeQuestion({
       name: "Question",
-      query: { "source-table": PRODUCTS_ID },
-      limit: 1,
+      native: {
+        query: `
+          SELECT *
+          FROM PRODUCTS
+          WHERE true
+            [[AND created_at > {{created_at}}]]
+            [[AND price > {{price}}]]
+            [[AND category = {{category}}]]`,
+        "template-tags": {
+          date: {
+            type: "date",
+            name: "created_at",
+            id: "b2517f32-d2e2-4f42-ab79-c91e07e820a0",
+            "display-name": "Created At",
+          },
+          price: {
+            type: "number",
+            name: "price",
+            id: "879d1597-e673-414c-a96f-ff5887359834",
+            "display-name": "Price",
+          },
+          category: {
+            type: "text",
+            name: "category",
+            id: "1f741a9a-a95e-4ac6-b584-5101e7cf77e1",
+            "display-name": "Category",
+          },
+        },
+      },
+      limit: 10,
     });
   }
 
   if (resource === "dashboard") {
-    return cy.createDashboard({ name: "Dashboard" });
+    const dateFilter = {
+      id: "1",
+      name: "Created At",
+      slug: "created_at",
+      type: "date/month-year",
+    };
+
+    const numberFilter = {
+      id: "2",
+      name: "Price",
+      slug: "price",
+      type: "number/=",
+    };
+
+    const textFilter = {
+      id: "3",
+      name: "Category",
+      slug: "category",
+      type: "string/contains",
+    };
+
+    return cy.createDashboard({
+      name: "Dashboard",
+      parameters: [dateFilter, numberFilter, textFilter],
+    });
   }
 }
 
 function createPublicResourceLink(resource, id) {
-  if (resource === "card") {
+  if (resource === "question") {
     return createPublicQuestionLink(id);
   }
   if (resource === "dashboard") {
@@ -280,7 +801,7 @@ function createPublicResourceLink(resource, id) {
 }
 
 function visitResource(resource, id) {
-  if (resource === "card") {
+  if (resource === "question") {
     visitQuestion(id);
   }
 
@@ -288,3 +809,16 @@ function visitResource(resource, id) {
     visitDashboard(id);
   }
 }
+
+function enableEmbeddingForResource({ resource, id }) {
+  const endpoint = resource === "question" ? "card" : "dashboard";
+  cy.request("PUT", `/api/${endpoint}/${id}`, {
+    enable_embedding: true,
+  });
+}
+
+function closeTo(value, offset) {
+  return comparedValue => {
+    return Math.abs(comparedValue - value) <= offset;
+  };
+}
diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts
index 8dafa3052a5..6e907352509 100644
--- a/frontend/src/metabase-types/api/card.ts
+++ b/frontend/src/metabase-types/api/card.ts
@@ -12,6 +12,8 @@ export type CardType = "model" | "question";
 export interface Card<Q extends DatasetQuery = DatasetQuery>
   extends UnsavedCard<Q> {
   id: CardId;
+  created_at: string;
+  updated_at: string;
   name: string;
   description: string | null;
   /**
@@ -24,6 +26,7 @@ export interface Card<Q extends DatasetQuery = DatasetQuery>
   /* Indicates whether static embedding for this card has been published */
   enable_embedding: boolean;
   can_write: boolean;
+  initially_published_at: string | null;
 
   database_id?: DatabaseId;
   collection?: Collection | null;
diff --git a/frontend/src/metabase-types/api/dashboard.ts b/frontend/src/metabase-types/api/dashboard.ts
index c20df180634..fc387cd95b9 100644
--- a/frontend/src/metabase-types/api/dashboard.ts
+++ b/frontend/src/metabase-types/api/dashboard.ts
@@ -23,6 +23,8 @@ export type DashboardCard =
 
 export interface Dashboard {
   id: DashboardId;
+  created_at: string;
+  updated_at: string;
   collection?: Collection | null;
   collection_id: number | null;
   name: string;
@@ -44,8 +46,9 @@ export interface Dashboard {
   auto_apply_filters: boolean;
   archived: boolean;
   public_uuid: string | null;
-  width: "full" | "fixed";
+  initially_published_at: string | null;
   embedding_params?: EmbeddingParameters | null;
+  width: "full" | "fixed";
 
   /* Indicates whether static embedding for this dashboard has been published */
   enable_embedding: boolean;
diff --git a/frontend/src/metabase-types/api/mocks/card.ts b/frontend/src/metabase-types/api/mocks/card.ts
index 60edf49f58f..6d32b45069b 100644
--- a/frontend/src/metabase-types/api/mocks/card.ts
+++ b/frontend/src/metabase-types/api/mocks/card.ts
@@ -17,6 +17,8 @@ import {
 
 export const createMockCard = (opts?: Partial<Card>): Card => ({
   id: 1,
+  created_at: "2024-01-01T00:00:00Z",
+  updated_at: "2024-01-01T00:00:00Z",
   name: "Question",
   description: null,
   display: "table",
@@ -35,6 +37,7 @@ export const createMockCard = (opts?: Partial<Card>): Card => ({
   based_on_upload: null,
   archived: false,
   enable_embedding: false,
+  initially_published_at: null,
   ...opts,
 });
 
diff --git a/frontend/src/metabase-types/api/mocks/dashboard.ts b/frontend/src/metabase-types/api/mocks/dashboard.ts
index a3a79ef766d..053271ef72c 100644
--- a/frontend/src/metabase-types/api/mocks/dashboard.ts
+++ b/frontend/src/metabase-types/api/mocks/dashboard.ts
@@ -10,6 +10,8 @@ import { createMockCard } from "./card";
 
 export const createMockDashboard = (opts?: Partial<Dashboard>): Dashboard => ({
   id: 1,
+  created_at: "2024-01-01T00:00:00Z",
+  updated_at: "2024-01-01T00:00:00Z",
   collection_id: null,
   name: "Dashboard",
   dashcards: [],
@@ -28,6 +30,7 @@ export const createMockDashboard = (opts?: Partial<Dashboard>): Dashboard => ({
   public_uuid: null,
   enable_embedding: false,
   embedding_params: null,
+  initially_published_at: null,
   width: "fixed",
   ...opts,
 });
diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
index 6ff0d604cac..37af1219d53 100644
--- a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
@@ -59,6 +59,8 @@ function convertActionToQuestionCard(
 ): Card<NativeDatasetQuery> {
   return {
     id: action.id,
+    created_at: action.created_at,
+    updated_at: action.updated_at,
     name: action.name,
     description: action.description,
     dataset_query: action.dataset_query,
@@ -77,6 +79,7 @@ function convertActionToQuestionCard(
     average_query_time: null,
     archived: false,
     enable_embedding: false,
+    initially_published_at: null,
   };
 }
 
diff --git a/frontend/src/metabase/dashboard/actions/sharing.ts b/frontend/src/metabase/dashboard/actions/sharing.ts
index 3317e3e5b98..06f2619de15 100644
--- a/frontend/src/metabase/dashboard/actions/sharing.ts
+++ b/frontend/src/metabase/dashboard/actions/sharing.ts
@@ -29,7 +29,10 @@ export const UPDATE_ENABLE_EMBEDDING =
 export const updateEnableEmbedding = createAction(
   UPDATE_ENABLE_EMBEDDING,
   ({ id }: DashboardIdPayload, enable_embedding: boolean) =>
-    DashboardApi.update({ id, enable_embedding }),
+    DashboardApi.update({
+      id,
+      enable_embedding,
+    }),
 );
 
 export const UPDATE_EMBEDDING_PARAMS =
diff --git a/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts b/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
index c204f28dd95..d0029e2b1ed 100644
--- a/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
+++ b/frontend/src/metabase/dashboard/components/DashboardTabs/test-utils.ts
@@ -24,6 +24,8 @@ export const TEST_DASHBOARD_STATE: DashboardState = {
   dashboards: {
     1: {
       id: 1,
+      created_at: "2024-01-01T00:00:00Z",
+      updated_at: "2024-01-01T00:00:00Z",
       collection_id: 1,
       name: "",
       description: "",
@@ -46,6 +48,7 @@ export const TEST_DASHBOARD_STATE: DashboardState = {
       ],
       public_uuid: null,
       enable_embedding: false,
+      initially_published_at: null,
       width: "fixed",
     },
   },
diff --git a/frontend/src/metabase/dashboard/components/PublicLinkPopover/DashboardPublicLinkPopover.tsx b/frontend/src/metabase/dashboard/components/PublicLinkPopover/DashboardPublicLinkPopover.tsx
index 92a2fe98070..2b62f9fd634 100644
--- a/frontend/src/metabase/dashboard/components/PublicLinkPopover/DashboardPublicLinkPopover.tsx
+++ b/frontend/src/metabase/dashboard/components/PublicLinkPopover/DashboardPublicLinkPopover.tsx
@@ -2,6 +2,10 @@ import type { Dashboard } from "metabase-types/api";
 import { createPublicLink, deletePublicLink } from "metabase/dashboard/actions";
 import { useDispatch } from "metabase/lib/redux";
 import { publicDashboard as getPublicDashboardUrl } from "metabase/lib/urls";
+import {
+  trackPublicLinkCopied,
+  trackPublicLinkRemoved,
+} from "metabase/public/lib/analytics";
 import { PublicLinkPopover } from "./PublicLinkPopover";
 
 export const DashboardPublicLinkPopover = ({
@@ -25,9 +29,19 @@ export const DashboardPublicLinkPopover = ({
     await dispatch(createPublicLink(dashboard));
   };
   const deletePublicDashboardLink = () => {
+    trackPublicLinkRemoved({
+      artifact: "dashboard",
+      source: "public-share",
+    });
     dispatch(deletePublicLink(dashboard));
   };
 
+  const onCopyLink = () => {
+    trackPublicLinkCopied({
+      artifact: "dashboard",
+    });
+  };
+
   return (
     <PublicLinkPopover
       target={target}
@@ -36,6 +50,7 @@ export const DashboardPublicLinkPopover = ({
       createPublicLink={createPublicDashboardLink}
       deletePublicLink={deletePublicDashboardLink}
       url={url}
+      onCopyLink={onCopyLink}
     />
   );
 };
diff --git a/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkCopyPanel.tsx b/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkCopyPanel.tsx
index 7ccff209cfa..4d264c5f3d8 100644
--- a/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkCopyPanel.tsx
+++ b/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkCopyPanel.tsx
@@ -24,6 +24,7 @@ export const PublicLinkCopyPanel = ({
   onChangeExtension,
   removeButtonLabel,
   removeTooltipLabel,
+  onCopy,
 }: {
   loading?: boolean;
   url: string | null;
@@ -33,6 +34,7 @@ export const PublicLinkCopyPanel = ({
   extensions?: ExportFormatType[];
   removeButtonLabel?: string;
   removeTooltipLabel?: string;
+  onCopy?: () => void;
 }) => (
   <Stack spacing={0}>
     <TextInput
@@ -41,7 +43,7 @@ export const PublicLinkCopyPanel = ({
       placeholder={loading ? t`Loading…` : undefined}
       value={url ?? undefined}
       inputWrapperOrder={["label", "input", "error", "description"]}
-      rightSection={url && <PublicLinkCopyButton value={url} />}
+      rightSection={url && <PublicLinkCopyButton value={url} onCopy={onCopy} />}
     />
     <Box pos="relative">
       <Group mt="sm" pos="absolute" w="100%" position="apart" align="center">
@@ -56,6 +58,7 @@ export const PublicLinkCopyPanel = ({
               }
             >
               <RemoveLinkAnchor
+                component="button"
                 fz="sm"
                 c="error"
                 fw={700}
diff --git a/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkPopover.tsx b/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkPopover.tsx
index f1fd28f9a78..b434b664e36 100644
--- a/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkPopover.tsx
+++ b/frontend/src/metabase/dashboard/components/PublicLinkPopover/PublicLinkPopover.tsx
@@ -16,6 +16,7 @@ export type PublicLinkPopoverProps = {
   extensions?: ExportFormatType[];
   selectedExtension?: ExportFormatType | null;
   setSelectedExtension?: (extension: ExportFormatType) => void;
+  onCopyLink?: () => void;
 };
 
 export const PublicLinkPopover = ({
@@ -28,6 +29,7 @@ export const PublicLinkPopover = ({
   extensions = [],
   selectedExtension,
   setSelectedExtension,
+  onCopyLink,
 }: PublicLinkPopoverProps) => {
   const isAdmin = useSelector(getUserIsAdmin);
 
@@ -78,6 +80,7 @@ export const PublicLinkPopover = ({
             onChangeExtension={setSelectedExtension}
             removeButtonLabel={t`Remove public link`}
             removeTooltipLabel={t`Affects both public link and embed URL for this dashboard`}
+            onCopy={onCopyLink}
           />
         </Box>
       </Popover.Dropdown>
diff --git a/frontend/src/metabase/dashboard/components/PublicLinkPopover/QuestionPublicLinkPopover.tsx b/frontend/src/metabase/dashboard/components/PublicLinkPopover/QuestionPublicLinkPopover.tsx
index e423eb6277f..268847371b5 100644
--- a/frontend/src/metabase/dashboard/components/PublicLinkPopover/QuestionPublicLinkPopover.tsx
+++ b/frontend/src/metabase/dashboard/components/PublicLinkPopover/QuestionPublicLinkPopover.tsx
@@ -1,4 +1,8 @@
 import { useState } from "react";
+import {
+  trackPublicLinkCopied,
+  trackPublicLinkRemoved,
+} from "metabase/public/lib/analytics";
 import { useDispatch } from "metabase/lib/redux";
 import {
   exportFormats,
@@ -40,9 +44,20 @@ export const QuestionPublicLinkPopover = ({
     await dispatch(createPublicLink(question.card()));
   };
   const deletePublicQuestionLink = async () => {
+    trackPublicLinkRemoved({
+      artifact: "question",
+      source: "public-share",
+    });
     await dispatch(deletePublicLink(question.card()));
   };
 
+  const onCopyLink = () => {
+    trackPublicLinkCopied({
+      artifact: "question",
+      format: extension ?? "html",
+    });
+  };
+
   return (
     <PublicLinkPopover
       target={target}
@@ -54,6 +69,7 @@ export const QuestionPublicLinkPopover = ({
       extensions={exportFormats}
       selectedExtension={extension}
       setSelectedExtension={setExtension}
+      onCopyLink={onCopyLink}
     />
   );
 };
diff --git a/frontend/src/metabase/dashboard/reducers.js b/frontend/src/metabase/dashboard/reducers.js
index aeed41833df..4f1f44223cb 100644
--- a/frontend/src/metabase/dashboard/reducers.js
+++ b/frontend/src/metabase/dashboard/reducers.js
@@ -153,11 +153,11 @@ const dashboards = handleActions(
     },
     [UPDATE_ENABLE_EMBEDDING]: {
       next: (state, { payload }) =>
-        assocIn(
-          state,
-          [payload.id, "enable_embedding"],
-          payload.enable_embedding,
-        ),
+        produce(state, draftState => {
+          const dashboard = draftState[payload.id];
+          dashboard.enable_embedding = payload.enable_embedding;
+          dashboard.initially_published_at = payload.initially_published_at;
+        }),
     },
     [Dashboards.actionTypes.UPDATE]: {
       next: (state, { payload }) => {
diff --git a/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SelectEmbedTypePane.tsx b/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SelectEmbedTypePane.tsx
index a02de7f3a24..ddb6b60c4e3 100644
--- a/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SelectEmbedTypePane.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SelectEmbedTypePane.tsx
@@ -1,6 +1,10 @@
 import type { MouseEvent } from "react";
 import { useState } from "react";
 import { t } from "ttag";
+import {
+  trackPublicEmbedCodeCopied,
+  trackPublicLinkRemoved,
+} from "metabase/public/lib/analytics";
 import { PublicLinkCopyPanel } from "metabase/dashboard/components/PublicLinkPopover/PublicLinkCopyPanel";
 import { useSelector } from "metabase/lib/redux";
 import { getSetting } from "metabase/selectors/settings";
@@ -67,6 +71,12 @@ export function SelectEmbedTypePane({
         "Public Link Disabled",
         resourceType,
       );
+
+      trackPublicLinkRemoved({
+        artifact: resourceType,
+        source: "public-embed",
+      });
+
       await onDeletePublicLink();
       setIsLoadingLink(false);
     }
@@ -96,6 +106,12 @@ export function SelectEmbedTypePane({
       return (
         <PublicLinkCopyPanel
           url={iframeSource}
+          onCopy={() =>
+            trackPublicEmbedCodeCopied({
+              artifact: resourceType,
+              source: "public-embed",
+            })
+          }
           onRemoveLink={deletePublicLink}
           removeButtonLabel={t`Remove public URL`}
           removeTooltipLabel={t`Affects both embed URL and public link for this dashboard`}
@@ -146,6 +162,7 @@ export function SelectEmbedTypePane({
           }
           disabled={!isPublicSharingEnabled}
           onClick={createPublicLink}
+          data-testid="sharing-pane-public-embed-button"
           illustration={<PublicEmbedIcon disabled={!isPublicSharingEnabled} />}
         >
           {getPublicLinkElement()}
diff --git a/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SharingPaneButton/SharingPaneButton.tsx b/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SharingPaneButton/SharingPaneButton.tsx
index faf21ba9165..55cdc44f0e4 100644
--- a/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SharingPaneButton/SharingPaneButton.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/SelectEmbedTypePane/SharingPaneButton/SharingPaneButton.tsx
@@ -13,6 +13,7 @@ type SharingOptionProps = {
   description: ReactNode | string;
   disabled?: boolean;
   onClick?: MouseEventHandler;
+  "data-testid"?: string;
 };
 
 export const SharingPaneButton = ({
@@ -22,8 +23,13 @@ export const SharingPaneButton = ({
   description,
   disabled,
   onClick,
+  "data-testid": dataTestId,
 }: SharingOptionProps) => (
-  <SharingPaneButtonContent withBorder disabled={disabled}>
+  <SharingPaneButtonContent
+    withBorder
+    disabled={disabled}
+    data-testid={dataTestId}
+  >
     <Center
       h="22.5rem"
       p="8rem"
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/AppearanceSettings.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/AppearanceSettings.tsx
index 8e7ab4dbcba..65e1d4e17a7 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/AppearanceSettings.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/AppearanceSettings.tsx
@@ -25,8 +25,8 @@ const THEME_OPTIONS = [
   { label: t`Light`, value: "light" },
   { label: t`Dark`, value: "night" },
   { label: t`Transparent`, value: "transparent" },
-];
-const DEFAULT_THEME = THEME_OPTIONS[0].value;
+] as const;
+type ThemeOptions = typeof THEME_OPTIONS[number]["value"];
 
 export interface AppearanceSettingsProps {
   resourceType: EmbedResourceType;
@@ -59,6 +59,7 @@ export const AppearanceSettings = ({
   const utmTags = `?utm_source=${plan}&utm_media=static-embed-settings-appearance`;
 
   const fontControlLabelId = useUniqueId("display-option");
+  const downloadDataId = useUniqueId("download-data");
 
   return (
     <>
@@ -79,23 +80,22 @@ export const AppearanceSettings = ({
         <Stack spacing="1rem">
           <DisplayOptionSection title={t`Background`}>
             <SegmentedControl
-              value={displayOptions.theme || DEFAULT_THEME}
-              data={THEME_OPTIONS}
+              value={displayOptions.theme}
+              // `data` type is required to be mutable, but THEME_OPTIONS is const.
+              data={[...THEME_OPTIONS]}
               fullWidth
               bg={color("bg-light")}
-              onChange={value => {
-                const newValue = value === DEFAULT_THEME ? null : value;
-
+              onChange={(value: ThemeOptions) => {
                 onChangeDisplayOptions({
                   ...displayOptions,
-                  theme: newValue,
+                  theme: value,
                 });
               }}
             />
           </DisplayOptionSection>
 
           <Switch
-            label={t`Dashboard title`}
+            label={getTitleLabel(resourceType)}
             labelPosition="left"
             size="sm"
             variant="stretch"
@@ -159,8 +159,12 @@ export const AppearanceSettings = ({
           {canWhitelabel && resourceType === "question" && (
             // We only show the "Download Data" toggle if the users are pro/enterprise
             // and they're sharing a question metabase#23477
-            <DisplayOptionSection title={t`Download data`}>
+            <DisplayOptionSection
+              title={t`Download data`}
+              titleId={downloadDataId}
+            >
               <Switch
+                aria-labelledby={downloadDataId}
                 label={t`Enable users to download data from this embed`}
                 labelPosition="left"
                 size="sm"
@@ -169,7 +173,7 @@ export const AppearanceSettings = ({
                 onChange={e =>
                   onChangeDisplayOptions({
                     ...displayOptions,
-                    hide_download_button: !e.target.checked ? true : null,
+                    hide_download_button: !e.target.checked,
                   })
                 }
               />
@@ -195,3 +199,15 @@ export const AppearanceSettings = ({
     </>
   );
 };
+
+function getTitleLabel(resourceType: EmbedResourceType) {
+  if (resourceType === "dashboard") {
+    return t`Dashboard title`;
+  }
+
+  if (resourceType === "question") {
+    return t`Question title`;
+  }
+
+  return null;
+}
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ClientEmbedCodePane.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ClientEmbedCodePane.tsx
index 47879860fa2..de7c7e99ae4 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ClientEmbedCodePane.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ClientEmbedCodePane.tsx
@@ -10,17 +10,19 @@ import "ace/mode-html_ruby";
 
 interface ClientEmbedCodePaneProps {
   clientCodeOptions: ClientCodeSampleConfig[];
-  selectedClientCodeOptionName: string;
-  setSelectedClientCodeOptionName: (languageName: string) => void;
+  selectedClientCodeOptionId: string;
+  setSelectedClientCodeOptionId: (languageName: string) => void;
+  onCopy: () => void;
 }
 
 export const ClientEmbedCodePane = ({
   clientCodeOptions,
-  selectedClientCodeOptionName,
-  setSelectedClientCodeOptionName,
+  selectedClientCodeOptionId,
+  setSelectedClientCodeOptionId,
+  onCopy,
 }: ClientEmbedCodePaneProps): JSX.Element | null => {
   const selectedClientCodeOption = clientCodeOptions.find(
-    ({ name }) => name === selectedClientCodeOptionName,
+    ({ id }) => id === selectedClientCodeOptionId,
   );
 
   if (!selectedClientCodeOption) {
@@ -31,11 +33,12 @@ export const ClientEmbedCodePane = ({
     <CodeSample
       dataTestId="embed-frontend"
       title={t`Then insert this code snippet in your HTML template or single page app.`}
-      selectedOptionName={selectedClientCodeOptionName}
-      languageOptions={clientCodeOptions.map(({ name }) => name)}
+      selectedOptionId={selectedClientCodeOptionId}
+      languageOptions={clientCodeOptions}
       source={selectedClientCodeOption.source}
       textHighlightMode={selectedClientCodeOption.mode}
-      onChangeOption={setSelectedClientCodeOptionName}
+      onChangeOption={setSelectedClientCodeOptionId}
+      onCopy={onCopy}
     />
   );
 };
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/CodeSample.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/CodeSample.tsx
index cc480b60881..b8870a2a507 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/CodeSample.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/CodeSample.tsx
@@ -7,9 +7,9 @@ import type { CodeSampleOption } from "metabase/public/lib/types";
 import { CopyButtonContainer } from "./CodeSample.styled";
 
 interface CodeSampleProps {
-  selectedOptionName: CodeSampleOption["name"];
+  selectedOptionId: CodeSampleOption["id"];
   source: string;
-  languageOptions: CodeSampleOption["name"][];
+  languageOptions: CodeSampleOption[];
   title?: string;
   textHighlightMode: string;
   highlightedTexts?: string[];
@@ -18,10 +18,11 @@ interface CodeSampleProps {
   className?: string;
 
   onChangeOption: (optionName: string) => void;
+  onCopy?: () => void;
 }
 
 export const CodeSample = ({
-  selectedOptionName,
+  selectedOptionId,
   source,
   title,
   languageOptions,
@@ -30,6 +31,7 @@ export const CodeSample = ({
   highlightedTexts,
   className,
   onChangeOption,
+  onCopy,
 }: CodeSampleProps): JSX.Element => {
   return (
     <div className={className} data-testid={dataTestId}>
@@ -39,7 +41,7 @@ export const CodeSample = ({
           {languageOptions.length > 1 ? (
             <Select
               className="AdminSelect--borderless ml-auto"
-              value={selectedOptionName}
+              value={selectedOptionId}
               onChange={(e: ChangeEvent<HTMLSelectElement>) =>
                 onChangeOption(e.target.value)
               }
@@ -48,8 +50,8 @@ export const CodeSample = ({
               }}
             >
               {languageOptions.map(option => (
-                <Option key={option} value={option}>
-                  {option}
+                <Option key={option.id} value={option.id}>
+                  {option.name}
                 </Option>
               ))}
             </Select>
@@ -68,7 +70,7 @@ export const CodeSample = ({
         />
         {source && (
           <CopyButtonContainer>
-            <CopyButton className="p1" value={source} />
+            <CopyButton className="p1" value={source} onCopy={onCopy} />
           </CopyButtonContainer>
         )}
       </div>
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/EmbedModalContentStatusBar.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/EmbedModalContentStatusBar.tsx
index b2f63342f45..227ca5e1512 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/EmbedModalContentStatusBar.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/EmbedModalContentStatusBar.tsx
@@ -24,7 +24,13 @@ export const EmbedModalContentStatusBar = ({
   const [isUnpublishing, setIsUnpublishing] = useState(false);
 
   return (
-    <Paper withBorder shadow="sm" m="1.5rem 2rem" p="0.75rem 1rem">
+    <Paper
+      withBorder
+      shadow="sm"
+      m="1.5rem 2rem"
+      p="0.75rem 1rem"
+      data-testid="embed-modal-content-status-bar"
+    >
       <Flex w="100%" justify="space-between" align="center" gap="0.5rem">
         <Text fw="bold">
           {!isPublished
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/OverviewSettings.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/OverviewSettings.tsx
index 82f0cb40cd6..ba764254f29 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/OverviewSettings.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/OverviewSettings.tsx
@@ -20,6 +20,7 @@ export interface OverviewSettingsProps {
   resourceType: EmbedResourceType;
   serverEmbedCodeSlot: ReactNode;
   selectedServerCodeOption: ServerCodeSampleConfig | undefined;
+  onClientCodeCopy: (language: string) => void;
 }
 
 const clientCodeOptions = getEmbedClientCodeExampleOptions();
@@ -28,6 +29,7 @@ export const OverviewSettings = ({
   resourceType,
   serverEmbedCodeSlot,
   selectedServerCodeOption,
+  onClientCodeCopy,
 }: OverviewSettingsProps): JSX.Element => {
   const docsUrl = useSelector(state =>
     // eslint-disable-next-line no-unconditional-metabase-links-render -- Only appear to admins
@@ -37,8 +39,9 @@ export const OverviewSettings = ({
     getPlan(getSetting(state, "token-features")),
   );
 
-  const [selectedClientCodeOptionName, setSelectedClientCodeOptionName] =
-    useState(clientCodeOptions[0].name);
+  const [selectedClientCodeOptionId, setSelectedClientCodeOptionId] = useState(
+    clientCodeOptions[0].id,
+  );
 
   useEffect(() => {
     if (selectedServerCodeOption) {
@@ -46,9 +49,9 @@ export const OverviewSettings = ({
 
       if (
         embedOption &&
-        clientCodeOptions.find(({ name }) => name === embedOption)
+        clientCodeOptions.find(({ id }) => id === embedOption)
       ) {
-        setSelectedClientCodeOptionName(embedOption);
+        setSelectedClientCodeOptionId(embedOption);
       }
     }
   }, [selectedServerCodeOption]);
@@ -81,8 +84,9 @@ export const OverviewSettings = ({
 
           <ClientEmbedCodePane
             clientCodeOptions={clientCodeOptions}
-            selectedClientCodeOptionName={selectedClientCodeOptionName}
-            setSelectedClientCodeOptionName={setSelectedClientCodeOptionName}
+            selectedClientCodeOptionId={selectedClientCodeOptionId}
+            setSelectedClientCodeOptionId={setSelectedClientCodeOptionId}
+            onCopy={() => onClientCodeCopy(selectedClientCodeOptionId)}
           />
 
           <Box my="1rem">
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ServerEmbedCodePane.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ServerEmbedCodePane.tsx
index b0f42eb2c36..c07075389d9 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ServerEmbedCodePane.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/ServerEmbedCodePane.tsx
@@ -3,7 +3,7 @@ import { t } from "ttag";
 import { getEmbedServerCodeExampleOptions } from "metabase/public/lib/code";
 import type {
   EmbeddingDisplayOptions,
-  EmbeddingParameters,
+  EmbeddingParametersValues,
   EmbedResource,
   EmbedResourceType,
   ServerCodeSampleConfig,
@@ -14,8 +14,10 @@ import "ace/mode-javascript";
 import "ace/mode-ruby";
 import "ace/mode-python";
 
+import { useSelector } from "metabase/lib/redux";
+import { getCanWhitelabel } from "metabase/selectors/whitelabel";
 import type { EmbedCodePaneVariant } from "./types";
-import { DEFAULT_DISPLAY_OPTIONS } from "./config";
+import { getDefaultDisplayOptions } from "./config";
 import { CodeSample } from "./CodeSample";
 
 type EmbedCodePaneProps = {
@@ -24,13 +26,14 @@ type EmbedCodePaneProps = {
   variant: EmbedCodePaneVariant;
   resource: EmbedResource;
   resourceType: EmbedResourceType;
-  params: EmbeddingParameters;
+  params: EmbeddingParametersValues;
   displayOptions: EmbeddingDisplayOptions;
-  initialPreviewParameters: EmbeddingParameters;
+  initialPreviewParameters: EmbeddingParametersValues;
 
   serverCodeOptions: ServerCodeSampleConfig[];
-  selectedServerCodeOptionName: string;
-  setSelectedServerCodeOptionName: (languageName: string) => void;
+  selectedServerCodeOptionId: string;
+  setSelectedServerCodeOptionId: (languageName: string) => void;
+  onCopy: () => void;
 
   className?: string;
 };
@@ -45,15 +48,19 @@ export const ServerEmbedCodePane = ({
   displayOptions,
   initialPreviewParameters,
   serverCodeOptions,
-  selectedServerCodeOptionName,
-  setSelectedServerCodeOptionName,
+  selectedServerCodeOptionId,
+  setSelectedServerCodeOptionId,
+  onCopy,
 
   className,
 }: EmbedCodePaneProps): JSX.Element | null => {
   const selectedServerCodeOption = serverCodeOptions.find(
-    ({ name }) => name === selectedServerCodeOptionName,
+    ({ id }) => id === selectedServerCodeOptionId,
   );
 
+  const canWhitelabel = useSelector(getCanWhitelabel);
+  const shouldShowDownloadData = canWhitelabel && resourceType === "question";
+
   if (!selectedServerCodeOption) {
     return null;
   }
@@ -63,12 +70,13 @@ export const ServerEmbedCodePane = ({
       initialPreviewParameters,
       params,
       selectedServerCodeOption,
-      selectedServerCodeOptionName,
+      selectedServerCodeOptionId,
       siteUrl,
       secretKey,
       resourceType,
       resource,
       displayOptions,
+      shouldShowDownloadData,
     });
 
   const title = getTitle({
@@ -82,12 +90,13 @@ export const ServerEmbedCodePane = ({
       dataTestId="embed-backend"
       className={className}
       title={title}
-      selectedOptionName={selectedServerCodeOptionName}
-      languageOptions={serverCodeOptions.map(({ name }) => name)}
+      selectedOptionId={selectedServerCodeOptionId}
+      languageOptions={serverCodeOptions}
       source={selectedServerCodeOption.source}
       textHighlightMode={selectedServerCodeOption.mode}
       highlightedTexts={highlightedTexts}
-      onChangeOption={setSelectedServerCodeOptionName}
+      onChangeOption={setSelectedServerCodeOptionId}
+      onCopy={onCopy}
     />
   );
 };
@@ -96,23 +105,25 @@ function getHighlightedText({
   initialPreviewParameters,
   params,
   selectedServerCodeOption,
-  selectedServerCodeOptionName,
+  selectedServerCodeOptionId,
   siteUrl,
   secretKey,
   resourceType,
   resource,
   displayOptions,
+  shouldShowDownloadData,
 }: {
   siteUrl: string;
   secretKey: string;
   resource: EmbedResource;
   resourceType: EmbedResourceType;
-  params: EmbeddingParameters;
+  params: EmbeddingParametersValues;
   displayOptions: EmbeddingDisplayOptions;
-  initialPreviewParameters: EmbeddingParameters;
+  initialPreviewParameters: EmbeddingParametersValues;
 
   selectedServerCodeOption: ServerCodeSampleConfig;
-  selectedServerCodeOptionName: string;
+  selectedServerCodeOptionId: string;
+  shouldShowDownloadData: boolean;
 }) {
   const hasParametersCodeDiff =
     !_.isEqual(initialPreviewParameters, params) &&
@@ -124,11 +135,10 @@ function getHighlightedText({
         resourceId: resource.id,
         params: initialPreviewParameters,
         displayOptions,
-      }).find(({ name }) => name === selectedServerCodeOptionName)
-        ?.parametersSource;
+      }).find(({ id }) => id === selectedServerCodeOptionId)?.parametersSource;
 
   const hasAppearanceCodeDiff = !_.isEqual(
-    DEFAULT_DISPLAY_OPTIONS,
+    getDefaultDisplayOptions(shouldShowDownloadData),
     displayOptions,
   );
 
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/StaticEmbedSetupPane.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/StaticEmbedSetupPane.tsx
index 1db71d6acb4..1de885f281c 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/StaticEmbedSetupPane.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/StaticEmbedSetupPane.tsx
@@ -9,6 +9,7 @@ import type {
   EmbeddingDisplayOptions,
   EmbeddingParameters,
   EmbeddingParametersValues,
+  EmbeddingParameterVisibility,
   EmbedResource,
   EmbedResourceParameter,
   EmbedResourceType,
@@ -18,24 +19,37 @@ import {
   optionsToHashParams,
 } from "metabase/public/lib/embed";
 import { getEmbedServerCodeExampleOptions } from "metabase/public/lib/code";
-
+import {
+  trackStaticEmbedCodeCopied,
+  trackStaticEmbedDiscarded,
+  trackStaticEmbedPublished,
+  trackStaticEmbedUnpublished,
+} from "metabase/public/lib/analytics";
+import { getCanWhitelabel } from "metabase/selectors/whitelabel";
 import { getParameterValue } from "metabase-lib/parameters/utils/parameter-values";
-import { DEFAULT_DISPLAY_OPTIONS } from "./config";
+import { getDefaultDisplayOptions } from "./config";
 import { ServerEmbedCodePane } from "./ServerEmbedCodePane";
 import { EmbedModalContentStatusBar } from "./EmbedModalContentStatusBar";
 import { ParametersSettings } from "./ParametersSettings";
 import { AppearanceSettings } from "./AppearanceSettings";
 import { OverviewSettings } from "./OverviewSettings";
 import type { ActivePreviewPane, EmbedCodePaneVariant } from "./types";
+import { EMBED_MODAL_TABS } from "./tabs";
 import { SettingsTabLayout } from "./StaticEmbedSetupPane.styled";
 import { PreviewModeSelector } from "./PreviewModeSelector";
 import { PreviewPane } from "./PreviewPane";
 
-const TABS = {
-  Overview: "overview" as const,
-  Parameters: "parameters" as const,
-  Appearance: "appearance" as const,
-};
+const countEmbeddingParameterOptions = (embeddingParams: EmbeddingParameters) =>
+  Object.values(embeddingParams).reduce(
+    (acc, value) => {
+      acc[value] += 1;
+      return acc;
+    },
+    { disabled: 0, locked: 0, enabled: 0 } as Record<
+      EmbeddingParameterVisibility,
+      number
+    >,
+  );
 
 export interface StaticEmbedSetupPaneProps {
   resource: EmbedResource;
@@ -70,8 +84,11 @@ export const StaticEmbedSetupPane = ({
   );
   const [parameterValues, setParameterValues] =
     useState<EmbeddingParametersValues>({});
+
+  const canWhitelabel = useSelector(getCanWhitelabel);
+  const shouldShowDownloadData = canWhitelabel && resourceType === "question";
   const [displayOptions, setDisplayOptions] = useState<EmbeddingDisplayOptions>(
-    DEFAULT_DISPLAY_OPTIONS,
+    getDefaultDisplayOptions(shouldShowDownloadData),
   );
 
   const previewParametersBySlug = useMemo(
@@ -102,11 +119,12 @@ export const StaticEmbedSetupPane = ({
     displayOptions,
   });
 
-  const [selectedServerCodeOptionName, setSelectedServerCodeOptionName] =
-    useState(serverCodeOptions[0].name);
+  const [selectedServerCodeOptionId, setSelectedServerCodeOptionId] = useState(
+    serverCodeOptions[0].id,
+  );
 
   const selectedServerCodeOption = serverCodeOptions.find(
-    ({ name }) => name === selectedServerCodeOptionName,
+    ({ id }) => id === selectedServerCodeOptionId,
   );
 
   const hasSettingsChanges = getHasSettingsChanges({
@@ -141,36 +159,83 @@ export const StaticEmbedSetupPane = ({
       await onUpdateEnableEmbedding(true);
     }
     await onUpdateEmbeddingParams(embeddingParams);
+    trackStaticEmbedPublished({
+      artifact: resourceType,
+      resource,
+      params: countEmbeddingParameterOptions({
+        ...convertResourceParametersToEmbeddingParams(resourceParameters),
+        ...embeddingParams,
+      }),
+    });
   };
 
   const handleUnpublish = async () => {
     await onUpdateEnableEmbedding(false);
+    trackStaticEmbedUnpublished({
+      artifact: resourceType,
+      resource,
+    });
   };
 
   const handleDiscard = () => {
     setEmbeddingParams(getDefaultEmbeddingParams(resource, resourceParameters));
+    trackStaticEmbedDiscarded({
+      artifact: resourceType,
+    });
   };
 
-  const getServerEmbedCodePane = (variant: EmbedCodePaneVariant) => (
-    <ServerEmbedCodePane
-      className="flex-full w-full"
-      variant={variant}
-      initialPreviewParameters={initialPreviewParameters}
-      resource={resource}
-      resourceType={resourceType}
-      siteUrl={siteUrl}
-      secretKey={secretKey}
-      params={previewParametersBySlug}
-      displayOptions={displayOptions}
-      serverCodeOptions={serverCodeOptions}
-      selectedServerCodeOptionName={selectedServerCodeOptionName}
-      setSelectedServerCodeOptionName={setSelectedServerCodeOptionName}
-    />
-  );
+  const getServerEmbedCodePane = (variant: EmbedCodePaneVariant) => {
+    return (
+      <ServerEmbedCodePane
+        className="flex-full w-full"
+        variant={variant}
+        initialPreviewParameters={initialPreviewParameters}
+        resource={resource}
+        resourceType={resourceType}
+        siteUrl={siteUrl}
+        secretKey={secretKey}
+        params={previewParametersBySlug}
+        displayOptions={displayOptions}
+        serverCodeOptions={serverCodeOptions}
+        selectedServerCodeOptionId={selectedServerCodeOptionId}
+        setSelectedServerCodeOptionId={setSelectedServerCodeOptionId}
+        onCopy={() =>
+          handleCodeCopy({
+            code: "backend",
+            variant,
+            language: selectedServerCodeOptionId,
+          })
+        }
+      />
+    );
+  };
 
-  const [activeTab, setActiveTab] = useState<typeof TABS[keyof typeof TABS]>(
-    TABS.Overview,
-  );
+  const handleCodeCopy = ({
+    code,
+    variant,
+    language,
+  }: {
+    code: "backend" | "view";
+    variant: EmbedCodePaneVariant;
+    language: string;
+  }) => {
+    const locationMap = {
+      overview: "code_overview",
+      parameters: "code_params",
+      appearance: "code_appearance",
+    } as const;
+    trackStaticEmbedCodeCopied({
+      artifact: resourceType,
+      location: locationMap[variant],
+      code,
+      language,
+      displayOptions,
+    });
+  };
+
+  const [activeTab, setActiveTab] = useState<
+    typeof EMBED_MODAL_TABS[keyof typeof EMBED_MODAL_TABS]
+  >(EMBED_MODAL_TABS.Overview);
   return (
     <Stack spacing={0}>
       <EmbedModalContentStatusBar
@@ -182,19 +247,22 @@ export const StaticEmbedSetupPane = ({
         onDiscard={handleDiscard}
       />
 
-      <Tabs defaultValue={TABS.Overview} data-testid="embedding-preview">
+      <Tabs
+        defaultValue={EMBED_MODAL_TABS.Overview}
+        data-testid="embedding-preview"
+      >
         <Tabs.List p="0 1.5rem">
           <Tabs.Tab
-            value={TABS.Overview}
-            onClick={() => setActiveTab(TABS.Overview)}
+            value={EMBED_MODAL_TABS.Overview}
+            onClick={() => setActiveTab(EMBED_MODAL_TABS.Overview)}
           >{t`Overview`}</Tabs.Tab>
           <Tabs.Tab
-            value={TABS.Parameters}
-            onClick={() => setActiveTab(TABS.Parameters)}
+            value={EMBED_MODAL_TABS.Parameters}
+            onClick={() => setActiveTab(EMBED_MODAL_TABS.Parameters)}
           >{t`Parameters`}</Tabs.Tab>
           <Tabs.Tab
-            value={TABS.Appearance}
-            onClick={() => setActiveTab(TABS.Appearance)}
+            value={EMBED_MODAL_TABS.Appearance}
+            onClick={() => setActiveTab(EMBED_MODAL_TABS.Appearance)}
           >{t`Appearance`}</Tabs.Tab>
         </Tabs.List>
         {/**
@@ -211,13 +279,18 @@ export const StaticEmbedSetupPane = ({
          * different `Tabs.Panel` if you were to use it as Mantine suggests.
          */}
         <Tabs.Panel value={activeTab}>
-          {activeTab === TABS.Overview ? (
+          {activeTab === EMBED_MODAL_TABS.Overview ? (
             <OverviewSettings
               resourceType={resourceType}
               selectedServerCodeOption={selectedServerCodeOption}
-              serverEmbedCodeSlot={getServerEmbedCodePane(TABS.Overview)}
+              serverEmbedCodeSlot={getServerEmbedCodePane(
+                EMBED_MODAL_TABS.Overview,
+              )}
+              onClientCodeCopy={language =>
+                handleCodeCopy({ code: "view", variant: "overview", language })
+              }
             />
-          ) : activeTab === TABS.Parameters ? (
+          ) : activeTab === EMBED_MODAL_TABS.Parameters ? (
             <SettingsTabLayout
               settingsSlot={
                 <ParametersSettings
@@ -248,12 +321,12 @@ export const StaticEmbedSetupPane = ({
                     isTransparent={displayOptions.theme === "transparent"}
                   />
                   {activePane === "code"
-                    ? getServerEmbedCodePane(TABS.Parameters)
+                    ? getServerEmbedCodePane(EMBED_MODAL_TABS.Parameters)
                     : null}
                 </>
               }
             />
-          ) : activeTab === TABS.Appearance ? (
+          ) : activeTab === EMBED_MODAL_TABS.Appearance ? (
             <SettingsTabLayout
               settingsSlot={
                 <AppearanceSettings
@@ -275,7 +348,7 @@ export const StaticEmbedSetupPane = ({
                     isTransparent={displayOptions.theme === "transparent"}
                   />
                   {activePane === "code"
-                    ? getServerEmbedCodePane(TABS.Appearance)
+                    ? getServerEmbedCodePane(EMBED_MODAL_TABS.Appearance)
                     : null}
                 </>
               }
@@ -376,3 +449,14 @@ function getNonDisabledEmbeddingParams(
     return result;
   }, {} as EmbeddingParameters);
 }
+
+function convertResourceParametersToEmbeddingParams(
+  resourceParameters: EmbedResourceParameter[],
+) {
+  const embeddingParams: EmbeddingParameters = {};
+  for (const parameter of resourceParameters) {
+    embeddingParams[parameter.slug] = "disabled";
+  }
+
+  return embeddingParams;
+}
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/config.ts b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/config.ts
index 01be596df3d..9994ed4bd27 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/config.ts
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/config.ts
@@ -1,8 +1,13 @@
 import type { EmbeddingDisplayOptions } from "metabase/public/lib/types";
 
-export const DEFAULT_DISPLAY_OPTIONS: EmbeddingDisplayOptions = {
-  font: null,
-  theme: null,
-  bordered: true,
-  titled: true,
-};
+export function getDefaultDisplayOptions(
+  shouldShownDownloadData: boolean,
+): EmbeddingDisplayOptions {
+  return {
+    font: null,
+    theme: "light",
+    bordered: true,
+    titled: true,
+    hide_download_button: shouldShownDownloadData ? false : null,
+  };
+}
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tabs.ts b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tabs.ts
new file mode 100644
index 00000000000..5fdce66b08f
--- /dev/null
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tabs.ts
@@ -0,0 +1,5 @@
+export const EMBED_MODAL_TABS = {
+  Overview: "overview" as const,
+  Parameters: "parameters" as const,
+  Appearance: "appearance" as const,
+};
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/common.unit.spec.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/common.unit.spec.tsx
index def6d118524..0b0d5c7fb82 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/common.unit.spec.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/common.unit.spec.tsx
@@ -448,7 +448,11 @@ describe("Static Embed Setup phase", () => {
           `"#theme=transparent&bordered=true&titled=true"`,
         );
 
-        userEvent.click(screen.getByText("Dashboard title"));
+        userEvent.click(
+          screen.getByText(
+            resourceType === "dashboard" ? "Dashboard title" : "Question title",
+          ),
+        );
 
         expect(screen.getByTestId("text-editor-mock")).toHaveTextContent(
           `"#theme=transparent&bordered=true&titled=false"`,
diff --git a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/premium.unit.spec.tsx b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/premium.unit.spec.tsx
index 31449b102b0..696b98ef307 100644
--- a/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/premium.unit.spec.tsx
+++ b/frontend/src/metabase/public/components/EmbedModal/StaticEmbedSetupPane/tests/premium.unit.spec.tsx
@@ -118,15 +118,9 @@ describe("Static Embed Setup phase - EE, with token", () => {
           });
 
           expect(screen.getByText("Download data")).toBeVisible();
-          expect(
-            screen.getByLabelText(
-              "Enable users to download data from this embed",
-            ),
-          ).toBeChecked();
-
-          userEvent.click(
-            screen.getByText("Enable users to download data from this embed"),
-          );
+          expect(screen.getByLabelText("Download data")).toBeChecked();
+
+          userEvent.click(screen.getByLabelText("Download data"));
 
           expect(screen.getByTestId("text-editor-mock")).toHaveTextContent(
             `hide_download_button=true`,
diff --git a/frontend/src/metabase/public/lib/analytics.ts b/frontend/src/metabase/public/lib/analytics.ts
new file mode 100644
index 00000000000..01a1bde2cb5
--- /dev/null
+++ b/frontend/src/metabase/public/lib/analytics.ts
@@ -0,0 +1,154 @@
+import type { ExportFormatType } from "metabase/dashboard/components/PublicLinkPopover/types";
+import { trackSchemaEvent } from "metabase/lib/analytics";
+import type {
+  EmbeddingDisplayOptions,
+  EmbedResource,
+  EmbedResourceType,
+} from "./types";
+
+const SCHEMA_NAME = "embed_flow";
+const SCHEMA_VERSION = "1-0-0";
+
+type Appearance = {
+  titled: boolean;
+  bordered: boolean;
+  theme: "light" | "night" | "transparent";
+  font: "instance" | "custom";
+  hide_download_button: boolean | null;
+};
+
+export const trackStaticEmbedDiscarded = ({
+  artifact,
+}: {
+  artifact: EmbedResourceType;
+}): void => {
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "static_embed_discarded",
+    artifact,
+  });
+};
+
+export const trackStaticEmbedPublished = ({
+  artifact,
+  resource,
+  params,
+}: {
+  artifact: EmbedResourceType;
+  resource: EmbedResource;
+  params: Record<string, number>;
+}): void => {
+  const now = Date.now();
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "static_embed_published",
+    artifact,
+    new_embed: !resource.initially_published_at,
+    time_since_creation: toSecond(
+      now - new Date(resource.created_at).getTime(),
+    ),
+    time_since_initial_publication: resource.initially_published_at
+      ? toSecond(now - new Date(resource.initially_published_at).getTime())
+      : null,
+    params,
+  });
+};
+
+function toSecond(milliseconds: number) {
+  return Math.round(milliseconds / 1000);
+}
+
+export const trackStaticEmbedUnpublished = ({
+  artifact,
+  resource,
+}: {
+  artifact: EmbedResourceType;
+  resource: EmbedResource;
+}): void => {
+  const now = Date.now();
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "static_embed_unpublished",
+    artifact,
+    time_since_creation: toSecond(
+      now - new Date(resource.created_at).getTime(),
+    ),
+    time_since_initial_publication: resource.initially_published_at
+      ? toSecond(now - new Date(resource.initially_published_at).getTime())
+      : null,
+  });
+};
+
+export const trackStaticEmbedCodeCopied = ({
+  artifact,
+  language,
+  location,
+  code,
+  displayOptions,
+}: {
+  artifact: EmbedResourceType;
+  language: string;
+  location: "code_overview" | "code_params" | "code_appearance";
+  code: "backend" | "view";
+  displayOptions: EmbeddingDisplayOptions;
+}): void => {
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "static_embed_code_copied",
+    artifact,
+    language,
+    location,
+    code,
+    appearance: normalizeAppearance(displayOptions),
+  });
+};
+
+function normalizeAppearance(
+  displayOptions: EmbeddingDisplayOptions,
+): Appearance {
+  return {
+    titled: displayOptions.titled,
+    bordered: displayOptions.bordered,
+    theme: displayOptions.theme ?? "light",
+    font: displayOptions.font ? "custom" : "instance",
+    hide_download_button: displayOptions.hide_download_button,
+  };
+}
+
+export const trackPublicLinkCopied = ({
+  artifact,
+  format = null,
+}: {
+  artifact: EmbedResourceType;
+  format?: ExportFormatType | null;
+}): void => {
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "public_link_copied",
+    artifact,
+    format,
+  });
+};
+
+export const trackPublicEmbedCodeCopied = ({
+  artifact,
+  source,
+}: {
+  artifact: EmbedResourceType;
+  source: "public-embed" | "public-share";
+}): void => {
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "public_embed_code_copied",
+    artifact,
+    source,
+  });
+};
+
+export const trackPublicLinkRemoved = ({
+  artifact,
+  source,
+}: {
+  artifact: EmbedResourceType;
+  source: "public-embed" | "public-share";
+}): void => {
+  trackSchemaEvent(SCHEMA_NAME, SCHEMA_VERSION, {
+    event: "public_link_removed",
+    artifact,
+    source,
+  });
+};
diff --git a/frontend/src/metabase/public/lib/code-templates.ts b/frontend/src/metabase/public/lib/code-templates.ts
index 5a631dcb640..aab2d16aa85 100644
--- a/frontend/src/metabase/public/lib/code-templates.ts
+++ b/frontend/src/metabase/public/lib/code-templates.ts
@@ -1,16 +1,38 @@
 import type {
   CodeSampleParameters,
   EmbeddingDisplayOptions,
-  EmbeddingParameters,
+  EmbeddingParametersValues,
 } from "./types";
 import { optionsToHashParams } from "./embed";
 
+function getIframeQuerySource(displayOptions: EmbeddingDisplayOptions) {
+  return JSON.stringify(
+    optionsToHashParams(
+      removeDefaultValueParameters(displayOptions, {
+        theme: "light",
+        hide_download_button: false,
+      }),
+    ),
+  );
+}
+
+function removeDefaultValueParameters(
+  options: EmbeddingDisplayOptions,
+  defaultValues: Partial<EmbeddingDisplayOptions>,
+): Partial<EmbeddingDisplayOptions> {
+  return Object.fromEntries(
+    Object.entries(options).filter(
+      ([key, value]) =>
+        value !== defaultValues[key as keyof EmbeddingDisplayOptions],
+    ),
+  );
+}
+
 export const node = {
-  getParametersSource: (params: EmbeddingParameters) =>
+  getParametersSource: (params: EmbeddingParametersValues) =>
     `params: ${JSON.stringify(params, null, 2).split("\n").join("\n  ")},`,
 
-  getIframeQuerySource: (displayOptions: EmbeddingDisplayOptions) =>
-    JSON.stringify(optionsToHashParams(displayOptions)),
+  getIframeQuerySource,
 
   getServerSource: ({
     siteUrl,
@@ -39,15 +61,14 @@ var iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token +
 };
 
 export const python = {
-  getParametersSource: (params: EmbeddingParameters) =>
+  getParametersSource: (params: EmbeddingParametersValues) =>
     `"params": {
     ${Object.entries(params)
       .map(([key, value]) => JSON.stringify(key) + ": " + JSON.stringify(value))
       .join(",\n    ")}
   },`,
 
-  getIframeQuerySource: (displayOptions: EmbeddingDisplayOptions) =>
-    JSON.stringify(optionsToHashParams(displayOptions)),
+  getIframeQuerySource,
 
   getServerSource: ({
     siteUrl,
@@ -77,7 +98,7 @@ iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token +
 };
 
 export const ruby = {
-  getParametersSource: (params: EmbeddingParameters) =>
+  getParametersSource: (params: EmbeddingParametersValues) =>
     `:params => {
     ${Object.entries(params)
       .map(
@@ -89,8 +110,7 @@ export const ruby = {
       .join(",\n    ")}
   },`,
 
-  getIframeQuerySource: (displayOptions: EmbeddingDisplayOptions) =>
-    JSON.stringify(optionsToHashParams(displayOptions)),
+  getIframeQuerySource,
 
   getServerSource: ({
     siteUrl,
@@ -119,13 +139,12 @@ iframe_url = METABASE_SITE_URL + "/embed/${resourceType}/" + token +
 };
 
 export const clojure = {
-  getParametersSource: (params: EmbeddingParameters) =>
+  getParametersSource: (params: EmbeddingParametersValues) =>
     `:params   {${Object.entries(params)
       .map(([key, value]) => JSON.stringify(key) + " " + JSON.stringify(value))
       .join(",\n              ")}}`,
 
-  getIframeQuerySource: (displayOptions: EmbeddingDisplayOptions) =>
-    JSON.stringify(optionsToHashParams(displayOptions)),
+  getIframeQuerySource,
 
   getServerSource: ({
     siteUrl,
diff --git a/frontend/src/metabase/public/lib/code.ts b/frontend/src/metabase/public/lib/code.ts
index 3a44727c298..85b6d85b0e2 100644
--- a/frontend/src/metabase/public/lib/code.ts
+++ b/frontend/src/metabase/public/lib/code.ts
@@ -16,21 +16,25 @@ import {
 export const getEmbedClientCodeExampleOptions =
   (): ClientCodeSampleConfig[] => [
     {
+      id: "pug",
       name: "Pug / Jade",
       source: getPugSource({ iframeUrl: `iframeUrl` }),
       mode: "ace/mode/jade",
     },
     {
+      id: "mustache",
       name: "Mustache",
       source: getHtmlSource({ iframeUrl: `"{{iframeUrl}}"` }),
       mode: "ace/mode/html",
     },
     {
+      id: "erb",
       name: "ERB",
       source: getHtmlSource({ iframeUrl: `"<%= @iframe_url %>"` }),
       mode: "ace/mode/html_ruby",
     },
     {
+      id: "jsx",
       name: "JSX",
       source: getJsxSource({ iframeUrl: `{iframeUrl}` }),
       mode: "ace/mode/jsx",
@@ -41,6 +45,7 @@ export const getEmbedServerCodeExampleOptions = (
   codeSampleParameters: CodeSampleParameters,
 ): ServerCodeSampleConfig[] => [
   {
+    id: "node",
     name: "Node.js",
     source: node.getServerSource(codeSampleParameters),
     parametersSource: node.getParametersSource(codeSampleParameters.params),
@@ -48,9 +53,10 @@ export const getEmbedServerCodeExampleOptions = (
       codeSampleParameters.displayOptions,
     ),
     mode: "ace/mode/javascript",
-    embedOption: "Pug / Jade",
+    embedOption: "pug",
   },
   {
+    id: "ruby",
     name: "Ruby",
     source: ruby.getServerSource(codeSampleParameters),
     parametersSource: ruby.getParametersSource(codeSampleParameters.params),
@@ -58,9 +64,10 @@ export const getEmbedServerCodeExampleOptions = (
       codeSampleParameters.displayOptions,
     ),
     mode: "ace/mode/ruby",
-    embedOption: "ERB",
+    embedOption: "erb",
   },
   {
+    id: "python",
     name: "Python",
     source: python.getServerSource(codeSampleParameters),
     parametersSource: python.getParametersSource(codeSampleParameters.params),
@@ -68,9 +75,10 @@ export const getEmbedServerCodeExampleOptions = (
       codeSampleParameters.displayOptions,
     ),
     mode: "ace/mode/python",
-    embedOption: "Pug / Jade",
+    embedOption: "pug",
   },
   {
+    id: "clojure",
     name: "Clojure",
     source: clojure.getServerSource(codeSampleParameters),
     parametersSource: clojure.getParametersSource(codeSampleParameters.params),
@@ -78,7 +86,7 @@ export const getEmbedServerCodeExampleOptions = (
       codeSampleParameters.displayOptions,
     ),
     mode: "ace/mode/clojure",
-    embedOption: "Pug / Jade",
+    embedOption: "pug",
   },
 ];
 
diff --git a/frontend/src/metabase/public/lib/embed.ts b/frontend/src/metabase/public/lib/embed.ts
index cf0f6f9027e..4e7e3eb3c39 100644
--- a/frontend/src/metabase/public/lib/embed.ts
+++ b/frontend/src/metabase/public/lib/embed.ts
@@ -4,15 +4,15 @@ import { KJUR } from "jsrsasign"; // using jsrsasign because jsonwebtoken doesn'
 import type {
   EmbedResourceType,
   EmbedResource,
-  EmbeddingParameters,
+  EmbeddingParametersValues,
 } from "./types";
 
 function getSignedToken(
   resourceType: EmbedResourceType,
   resourceId: EmbedResource["id"],
-  params: EmbeddingParameters = {},
+  params: EmbeddingParametersValues = {},
   secretKey: string,
-  previewEmbeddingParams: EmbeddingParameters,
+  previewEmbeddingParams: EmbeddingParametersValues,
 ) {
   const unsignedToken: Record<string, any> = {
     resource: { [resourceType]: resourceId },
@@ -32,9 +32,9 @@ export function getSignedPreviewUrlWithoutHash(
   siteUrl: string,
   resourceType: EmbedResourceType,
   resourceId: EmbedResource["id"],
-  params: EmbeddingParameters = {},
+  params: EmbeddingParametersValues = {},
   secretKey: string,
-  previewEmbeddingParams: EmbeddingParameters,
+  previewEmbeddingParams: EmbeddingParametersValues,
 ) {
   const token = getSignedToken(
     resourceType,
diff --git a/frontend/src/metabase/public/lib/types.ts b/frontend/src/metabase/public/lib/types.ts
index 7bc26225e5a..0b2e34ae220 100644
--- a/frontend/src/metabase/public/lib/types.ts
+++ b/frontend/src/metabase/public/lib/types.ts
@@ -25,10 +25,10 @@ export type EmbeddingParametersValues = Record<string, string>;
 
 export type EmbeddingDisplayOptions = {
   font: null | string;
-  theme: null | string;
+  theme: "light" | "night" | "transparent";
   bordered: boolean;
   titled: boolean;
-  hide_download_button?: true | null;
+  hide_download_button: boolean | null;
 };
 
 export type CodeSampleParameters = {
@@ -36,17 +36,19 @@ export type CodeSampleParameters = {
   secretKey: string;
   resourceType: EmbedResourceType;
   resourceId: EmbedResource["id"];
-  params: EmbeddingParameters;
+  params: EmbeddingParametersValues;
   displayOptions: EmbeddingDisplayOptions;
 };
 
 export type ClientCodeSampleConfig = {
+  id: string;
   name: string;
   source: string;
   mode: string;
 };
 
 export type ServerCodeSampleConfig = {
+  id: string;
   name: string;
   source: string;
   parametersSource: string;
diff --git a/frontend/src/metabase/query_builder/actions/sharing.ts b/frontend/src/metabase/query_builder/actions/sharing.ts
index 4b150a4d018..3cfdbd49218 100644
--- a/frontend/src/metabase/query_builder/actions/sharing.ts
+++ b/frontend/src/metabase/query_builder/actions/sharing.ts
@@ -32,7 +32,10 @@ export const UPDATE_ENABLE_EMBEDDING = "metabase/card/UPDATE_ENABLE_EMBEDDING";
 export const updateEnableEmbedding = createAction(
   UPDATE_ENABLE_EMBEDDING,
   ({ id }: CardIdPayload, enable_embedding: boolean) =>
-    CardApi.update({ id, enable_embedding }),
+    CardApi.update({
+      id,
+      enable_embedding,
+    }),
 );
 
 export const UPDATE_EMBEDDING_PARAMS = "metabase/card/UPDATE_EMBEDDING_PARAMS";
diff --git a/frontend/src/metabase/query_builder/reducers.js b/frontend/src/metabase/query_builder/reducers.js
index 746a0a7aaeb..b860bfc7803 100644
--- a/frontend/src/metabase/query_builder/reducers.js
+++ b/frontend/src/metabase/query_builder/reducers.js
@@ -399,6 +399,7 @@ export const card = handleActions(
       next: (state, { payload }) => ({
         ...state,
         embedding_params: payload.embedding_params,
+        initially_published_at: payload.initially_published_at,
       }),
     },
   },
diff --git a/frontend/src/metabase/ui/components/overlays/Popover/index.ts b/frontend/src/metabase/ui/components/overlays/Popover/index.ts
deleted file mode 100644
index 9fb2451a522..00000000000
--- a/frontend/src/metabase/ui/components/overlays/Popover/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { Popover } from "@mantine/core";
-export type { PopoverBaseProps, PopoverProps } from "@mantine/core";
-export { getPopoverOverrides } from "./Popover.styled";
diff --git a/frontend/src/metabase/ui/components/overlays/Popover/index.tsx b/frontend/src/metabase/ui/components/overlays/Popover/index.tsx
new file mode 100644
index 00000000000..0e9236799f1
--- /dev/null
+++ b/frontend/src/metabase/ui/components/overlays/Popover/index.tsx
@@ -0,0 +1,18 @@
+import { Popover } from "@mantine/core";
+import type { PopoverDropdownProps } from "@mantine/core";
+
+export type { PopoverBaseProps, PopoverProps } from "@mantine/core";
+export { getPopoverOverrides } from "./Popover.styled";
+
+const MantinePopoverDropdown = Popover.Dropdown;
+
+const PopoverDropdown = Object.assign(function PopoverDropdown(
+  props: PopoverDropdownProps,
+) {
+  return <MantinePopoverDropdown {...props} data-popover="mantine-popover" />;
+},
+MantinePopoverDropdown);
+
+Popover.Dropdown = PopoverDropdown as typeof MantinePopoverDropdown;
+
+export { Popover };
diff --git a/resources/migrations/001_update_migrations.yaml b/resources/migrations/001_update_migrations.yaml
index cb6260ffc3a..f8a532ef898 100644
--- a/resources/migrations/001_update_migrations.yaml
+++ b/resources/migrations/001_update_migrations.yaml
@@ -5317,6 +5317,38 @@ databaseChangeLog:
         - dbms:
             type: mysql,mariadb
 
+  - changeSet:
+      id: v49.2024-02-02T11:27:49
+      author: oisincoveney
+      comment: >-
+        Add report_card.initially_published_at
+      changes:
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: initially_published_at
+                  type: ${timestamp_type}
+                  constraints:
+                    nullable: true
+                  remarks: The timestamp when the card was first published in a static embed
+
+  - changeSet:
+        id: v49.2024-02-02T11:28:36
+        author: oisincoveney
+        comment: >-
+          Add report_dashboard.initially_published_at
+        changes:
+          - addColumn:
+              tableName: report_dashboard
+              columns:
+                - column:
+                    name: initially_published_at
+                    type: ${timestamp_type}
+                    constraints:
+                      nullable: true
+                    remarks: The timestamp when the dashboard was first published in a static embed
+
   - changeSet:
       id: v49.2024-02-07T21:52:01
       author: noahmoss
diff --git a/snowplow/iglu-client-embedded/schemas/com.metabase/embed_flow/jsonschema/1-0-0 b/snowplow/iglu-client-embedded/schemas/com.metabase/embed_flow/jsonschema/1-0-0
new file mode 100644
index 00000000000..361634dec2e
--- /dev/null
+++ b/snowplow/iglu-client-embedded/schemas/com.metabase/embed_flow/jsonschema/1-0-0
@@ -0,0 +1,198 @@
+{
+  "$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
+  "description": "User interactions with public links, static embedding, and public embedding",
+  "self": {
+    "vendor": "com.metabase",
+    "name": "embed_flow",
+    "format": "jsonschema",
+    "version": "1-0-0"
+  },
+  "type": "object",
+  "properties": {
+    "event": {
+      "type": "string",
+      "enum": [
+        "static_embed_discarded",
+        "static_embed_published",
+        "static_embed_unpublished",
+        "static_embed_code_copied",
+        "public_link_copied",
+        "public_embed_code_copied",
+        "public_link_removed"
+      ],
+      "description": "The type of event being recorded."
+    },
+    "artifact": {
+      "type": "string",
+      "enum": [
+        "dashboard",
+        "question"
+      ],
+      "description": "The type of artifact involved in the event (either a dashboard or a question)."
+    },
+    "new_embed": {
+      "type": [
+        "boolean",
+        "null"
+      ],
+      "description": "Indicates if the embed is new."
+    },
+    "params": {
+      "type": [
+        "object",
+        "null"
+      ],
+      "description": "Parameters related to the artifact.",
+      "properties": {
+        "locked": {
+          "type": "integer",
+          "description": "Number of locked parameters in the artifact.",
+          "minimum": 0,
+          "maximum": 2147483647
+        },
+        "editable": {
+          "type": "integer",
+          "description": "Number of editable parameters in the artifact.",
+          "minimum": 0,
+          "maximum": 2147483647
+        },
+        "disabled": {
+          "type": "integer",
+          "description": "Number of disabled parameters in the artifact.",
+          "minimum": 0,
+          "maximum": 2147483647
+        }
+      }
+    },
+    "first_published_at": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "format": "date-time",
+      "description": "The timestamp when the artifact was first published."
+    },
+    "language": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "description": "The backend or view language of the artifact.",
+      "maxLength": 1024
+    },
+    "location": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "enum": [
+        "code_overview",
+        "code_params",
+        "code_appearance"
+      ],
+      "description": "The location in the code where the event is triggered."
+    },
+    "code": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "enum": [
+        "backend",
+        "view"
+      ],
+      "description": "Type of the language of the artifact where the event is triggered."
+    },
+    "appearance": {
+      "type": [
+        "object",
+        "null"
+      ],
+      "description": "The appearance settings of the artifact.",
+      "properties": {
+        "title": {
+          "type": "boolean",
+          "description": "Indicates if the title is visible."
+        },
+        "border": {
+          "type": "boolean",
+          "description": "Indicates if the border is visible."
+        },
+        "theme": {
+          "type": [
+            "string"
+          ],
+          "enum": [
+            "light",
+            "night",
+            "transparent"
+          ],
+          "description": "The theme of the artifact's appearance."
+        },
+        "font": {
+          "type": "string",
+          "enum": [
+            "instance",
+            "custom"
+          ],
+          "description": "The font type used in the artifact."
+        },
+        "hide_download_button": {
+          "type": [
+            "boolean",
+            "null"
+          ],
+          "description": "Indicates if the download button is hidden. It will be null on OSS instance since it's not supported."
+        }
+      }
+    },
+    "format": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "enum": [
+        "html",
+        "csv",
+        "xlsx",
+        "json",
+        null
+      ],
+      "description": "The format of the public link."
+    },
+    "source": {
+      "type": [
+        "string",
+        "null"
+      ],
+      "enum": [
+        "public-embed",
+        "public-share"
+      ],
+      "description": "Location where the public link is copied from"
+    },
+    "time_since_creation": {
+      "description": "Number of seconds from the creation of the artifact until the event is triggered.",
+      "type": [
+        "number",
+        "null"
+      ],
+      "minimum": 0,
+      "maximum": 2147483647
+    },
+    "time_since_initial_publication": {
+      "description": "Number of seconds from the initial publication of the artifact until the event is triggered.",
+      "type": [
+        "number",
+        "null"
+      ],
+      "minimum": 0,
+      "maximum": 2147483647
+    }
+  },
+  "required": [
+    "event",
+    "artifact"
+  ],
+  "additionalProperties": false
+}
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 580a3c1ee6a..30c22e07e01 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -42,6 +42,7 @@
    [metabase.shared.util.i18n :refer [trs]]
    [metabase.sync.analyze.query-results :as qr]
    [metabase.util :as u]
+   [metabase.util.embed :refer [maybe-populate-initially-published-at]]
    [metabase.util.i18n :refer [tru]]
    [metabase.util.log :as log]
    [metabase.util.malli :as mu]
@@ -181,7 +182,7 @@
 ;;; --------------------------------------------------- Revisions ----------------------------------------------------
 
 (def ^:private excluded-columns-for-card-revision
-  [:id :created_at :updated_at :entity_id :creator_id :public_uuid :made_public_by_id :metabase_version
+  [:id :created_at :updated_at :entity_id :creator_id :public_uuid :made_public_by_id :metabase_version :initially_published_at
    ;; we'll use type now
    :dataset])
 
@@ -560,6 +561,7 @@
       populate-result-metadata
       pre-update
       populate-query-fields
+      maybe-populate-initially-published-at
       (dissoc :id)))
 
 ;; Cards don't normally get deleted (they get archived instead) so this mostly affects tests
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 0029ea39b17..9dc82ef5249 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -28,6 +28,7 @@
    [metabase.public-settings :as public-settings]
    [metabase.query-processor.async :as qp.async]
    [metabase.util :as u]
+   [metabase.util.embed :refer [maybe-populate-initially-published-at]]
    [metabase.util.i18n :as i18n :refer [deferred-tru deferred-trun tru]]
    [metabase.util.log :as log]
    [metabase.util.malli :as mu]
@@ -94,10 +95,11 @@
 
 (t2/define-before-update :model/Dashboard
   [dashboard]
-  (u/prog1 dashboard
+  (u/prog1 (maybe-populate-initially-published-at dashboard)
     (params/assert-valid-parameters dashboard)
     (parameter-card/upsert-or-delete-from-parameters! "dashboard" (:id dashboard) (:parameters dashboard))
-    (collection/check-collection-namespace Dashboard (:collection_id dashboard))))
+    (collection/check-collection-namespace Dashboard (:collection_id dashboard)))
+  (maybe-populate-initially-published-at dashboard))
 
 (defn- update-dashboard-subscription-pulses!
   "Updates the pulses' names and collection IDs, and syncs the PulseCards"
@@ -227,7 +229,7 @@
    ;;   lower-numbered positions appearing before higher numbered ones.
    ;; TODO: querying on stats we don't have any dashboard that has a position, maybe we could just drop it?
    :public_uuid :made_public_by_id
-   :position])
+   :position :initially_published_at])
 
 (def ^:private excluded-columns-for-dashcard-revision
   [:entity_id :created_at :updated_at :collection_authority_level])
diff --git a/src/metabase/util/embed.clj b/src/metabase/util/embed.clj
index 75dd068fd1a..489905aba0f 100644
--- a/src/metabase/util/embed.clj
+++ b/src/metabase/util/embed.clj
@@ -108,6 +108,13 @@
   (or (get-in unsigned-token keyseq)
       (throw (ex-info (tru "Token is missing value for keypath {0}" keyseq) {:status-code 400}))))
 
+(defn maybe-populate-initially-published-at
+  "Populate `initially_published_at` if embedding is set to true"
+  [{:keys [enable_embedding initially_published_at] :as card-or-dashboard}]
+  (cond-> card-or-dashboard
+    (and (true? enable_embedding) (nil? initially_published_at))
+    (assoc :initially_published_at :%now)))
+
 (defsetting show-static-embed-terms
   (deferred-tru "Check if the static embedding licensing should be hidden in the static embedding flow")
   :type    :boolean
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index e522e843285..48d674b369a 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -66,28 +66,29 @@
 
 (def card-defaults
   "The default card params."
-  {:archived            false
-   :collection_id       nil
-   :collection_position nil
-   :collection_preview  true
-   :dataset_query       {}
-   :dataset             false
-   :type                "question"
-   :description         nil
-   :display             "scalar"
-   :enable_embedding    false
-   :entity_id           nil
-   :embedding_params    nil
-   :made_public_by_id   nil
-   :parameters          []
-   :parameter_mappings  []
-   :moderation_reviews  ()
-   :public_uuid         nil
-   :query_type          nil
-   :cache_ttl           nil
-   :average_query_time  nil
-   :last_query_start    nil
-   :result_metadata     nil})
+  {:archived               false
+   :collection_id          nil
+   :collection_position    nil
+   :collection_preview     true
+   :dataset_query          {}
+   :dataset                false
+   :type                   "question"
+   :description            nil
+   :display                "scalar"
+   :enable_embedding       false
+   :initially_published_at nil
+   :entity_id              nil
+   :embedding_params       nil
+   :made_public_by_id      nil
+   :parameters             []
+   :parameter_mappings     []
+   :moderation_reviews     ()
+   :public_uuid            nil
+   :query_type             nil
+   :cache_ttl              nil
+   :average_query_time     nil
+   :last_query_start       nil
+   :result_metadata        nil})
 
 ;; Used in dashboard tests
 (def card-defaults-no-hydrate
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 1de1543f357..1ccfb496d37 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -202,6 +202,7 @@
    :description             nil
    :embedding_params        nil
    :enable_embedding        false
+   :initially_published_at  nil
    :entity_id               true
    :made_public_by_id       nil
    :parameters              []
diff --git a/test/metabase/util/embed_test.clj b/test/metabase/util/embed_test.clj
index 19a5648cbc3..dcc2316e998 100644
--- a/test/metabase/util/embed_test.clj
+++ b/test/metabase/util/embed_test.clj
@@ -3,12 +3,16 @@
    [buddy.sign.jwt :as jwt]
    [clojure.test :refer :all]
    [crypto.random :as crypto-random]
+   [java-time.api :as t]
    [metabase.config :as config]
    [metabase.public-settings.premium-features :as premium-features]
    [metabase.public-settings.premium-features-test
     :as premium-features-test]
    [metabase.test :as mt]
-   [metabase.util.embed :as embed]))
+   [metabase.util :as u]
+   [metabase.util.embed :as embed]
+   [toucan2.core :as t2]
+   [toucan2.tools.with-temp :as t2.with-temp]))
 
 (def ^:private ^String token-with-alg-none
   "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhZG1pbiI6dHJ1ZX0.3Dbtd6Z0yuSfw62fOzBGHyiL0BJp3pod_PZE-BBdR-I")
@@ -54,3 +58,26 @@
               (testing "should return false when the user has already accepted licensing terms"
                 (embed/show-static-embed-terms! false)
                 (is (= (embed/show-static-embed-terms) false))))))))))
+
+(deftest maybe-populate-initially-published-at-test
+  (let [now #t "2022-09-01T12:34:56Z"]
+    (doseq [model [:model/Card :model/Dashboard]]
+      (testing "should populate `initially_published_at` when a Card's enable_embedding is changed to true"
+        (t2.with-temp/with-temp [model card {:enable_embedding false}]
+          (is (= nil (:initially_published_at card)))
+          (t2/update! model (u/the-id card) {:enable_embedding true})
+          (is (not= nil (:initially_published_at (t2/select-one model :id (u/the-id card)))))))
+      (testing "should keep `initially_published_at` value when a Card's enable_embedding is changed to false"
+        (t2.with-temp/with-temp [model card {:enable_embedding true :initially_published_at now}]
+          (is (not= nil (:initially_published_at card)))
+          (t2/update! model (u/the-id card) {:enable_embedding false})
+          (is (= (t/offset-date-time now) (:initially_published_at (t2/select-one model :id (u/the-id card)))))))
+      (testing "should keep `initially_published_at` value when `enable_embedding` is already set to true"
+        (t2.with-temp/with-temp [model card {:enable_embedding true :initially_published_at now}]
+          (t2/update! model (u/the-id card) {:enable_embedding true})
+          (is (= (t/offset-date-time now) (:initially_published_at (t2/select-one model :id (u/the-id card)))))))
+      (testing "should keep `initially_published_at` value when `enable_embedding` is already set to false"
+        (t2.with-temp/with-temp [model card {:enable_embedding false}]
+          (is (= nil (:initially_published_at card)))
+          (t2/update! model (u/the-id card) {:enable_embedding false})
+          (is (= nil (:initially_published_at (t2/select-one model :id (u/the-id card))))))))))
-- 
GitLab