fix(event-log): add event logging for embedded Superset (#41938)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luiz Otavio
2026-07-13 15:23:05 -03:00
committed by GitHub
parent 862aae398d
commit f8490fc9af
2 changed files with 51 additions and 4 deletions

View File

@@ -142,6 +142,38 @@ describe('logger middleware', () => {
}
});
const getSourceForPath = (href: string): string | undefined => {
const originalHref = window.location.href;
Object.defineProperty(window, 'location', {
value: { href },
writable: true,
});
try {
(logger as Function)(mockStore)(next)(action);
jest.advanceTimersByTime(2000);
const { events } = postStub.mock.calls[0][0].postPayload;
return events[0].source;
} finally {
Object.defineProperty(window, 'location', {
value: { href: originalHref },
writable: true,
});
}
};
test.each([
['/dashboard/123/embedded/', 'embedded_dashboard'],
['/embedded/abc-def-uuid/', 'embedded_dashboard'],
// React Router also matches these routes without a trailing slash
['/dashboard/123/embedded', 'embedded_dashboard'],
['/embedded/abc-def-uuid', 'embedded_dashboard'],
['/dashboard/123/', 'dashboard'],
// slug is literally "embedded" - must not be treated as embedded
['/dashboard/embedded/', 'dashboard'],
])('classifies %s as source "%s"', (path, expectedSource) => {
expect(getSourceForPath(`http://localhost${path}`)).toBe(expectedSource);
});
test('should debounce a few log requests to one', () => {
(logger as Function)(mockStore)(next)(action);
(logger as Function)(mockStore)(next)(action);

View File

@@ -33,7 +33,12 @@ import { ensureAppRoot } from '../utils/pathUtils';
import type { DashboardInfo, DashboardLayoutState } from '../dashboard/types';
import type { QueryEditor } from '../SqlLab/types';
type LogEventSource = 'dashboard' | 'explore' | 'sqlLab' | 'slice';
type LogEventSource =
| 'dashboard'
| 'embedded_dashboard'
| 'explore'
| 'sqlLab'
| 'slice';
interface LogEventData {
source?: LogEventSource;
@@ -99,7 +104,7 @@ const sendBeacon = (events: LogEventData[]): void => {
const [firstEvent] = events;
const { source, source_id } = firstEvent;
// backend logs treat these request params as first-class citizens
if (source === 'dashboard') {
if (source === 'dashboard' || source === 'embedded_dashboard') {
endpoint += `&dashboard_id=${source_id}`;
} else if (source === 'slice') {
endpoint += `&slice_id=${source_id}`;
@@ -131,6 +136,12 @@ const logMessageQueue = new DebouncedMessageQueue<LogEventData>({
delayThreshold: 1000,
});
// Embedded dashboards are served from `/dashboard/:idOrSlug/embedded/` and
// `/embedded/:uuid/`. Matching these specific shapes avoids false positives
// for regular dashboards (e.g. a dashboard whose slug is "embedded").
const EMBEDDED_ROUTE_REGEX =
/\/dashboard\/[^/]+\/embedded\/?|\/embedded\/[^/]+\/?/;
let lastEventId: string | number = 0;
const loggerMiddleware: Middleware<
@@ -162,9 +173,13 @@ const loggerMiddleware: Middleware<
}
const path = navPath || window?.location?.href;
if (dashboardInfo?.id && path?.includes('/dashboard/')) {
// Match the actual embedded route patterns (`/dashboard/:idOrSlug/embedded/`
// and `/embedded/:uuid/`) rather than any URL containing "/embedded/", which
// would misclassify a regular dashboard whose slug is "embedded".
const isEmbedded = EMBEDDED_ROUTE_REGEX.test(path ?? '');
if (dashboardInfo?.id && (path?.includes('/dashboard/') || isEmbedded)) {
logMetadata = {
source: 'dashboard',
source: isEmbedded ? 'embedded_dashboard' : 'dashboard',
source_id: dashboardInfo.id,
dashboard_id: dashboardInfo.id,
...logMetadata,