mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
feat(studio): track Explorer runs and saves (#51004)
I added outcome events for Explorer query runs and successful manual notebook saves. Existing page visits and preview toggles do not show whether users complete queries or persist notebooks. **Changed:** - **Query usage:** Accepted runs from query tabs and notebook cells emit submitted and terminal outcome events with a shared run ID. Canceled confirmations emit no run events. - **Notebook adoption:** Successful manual saves emit created or updated events. Recreated notebooks count as creations. Unsaved drafts and failed saves emit neither. - **Event metadata:** Explorer action events use `Explorer` as their page title. **Note:** Assistant-generated saves are outside this PR. Custom properties omit SQL and notebook content. Page visits still carry the browser title, which can include a notebook name. ## To test Tested on the staging preview: - [x] Run valid and invalid SQL from an Explorer query tab. Each run emits one submitted event and one matching completed or failed event with the same run ID. - [x] Run database and Logs notebook query cells, then add a markdown cell. The query cells emit matching event pairs; the markdown cell emits no query event. - [x] Save a new notebook, then edit and save it again. The successful saves emit created and updated events. - [x] Cancel a guarded query. It emits no query run event. - [ ] Recreate a notebook deleted on the server after local edits. A successful save emits created, not updated. - [x] Inspect an Explorer action event request. Its page title is `Explorer`; page visits still use the browser title. - [ ] Force a notebook save failure. It should emit no save event. This case was not tested manually. ## Linear - fixes GROWTH-1298 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Analytics** * Explorer query runs are tracked for database and log queries, including whether they complete or fail. * Query activity is associated with its location in Explorer, such as a query tab or notebook cell. * Successful notebook saves are tracked as creations or updates. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
13de8189c9
commit
4a941518e3
10 files changed
+307
-11
No files matched your search
+1
-1
@@ -34,7 +34,7 @@ reviews:
|
||||
Strictly enforce event naming: [object]_[verb] in snake_case. Only approved
|
||||
verbs: opened, clicked, submitted, created, removed, updated, retrieved,
|
||||
intended, evaluated, added, enabled, disabled, copied, exposed, failed,
|
||||
converted. Properties must be camelCase for new events (match existing
|
||||
converted, completed. Properties must be camelCase for new events (match existing
|
||||
convention when adding to existing events). Flag any usage of
|
||||
useSendEventMutation. Verify @group Events and @source JSDoc tags are
|
||||
accurate. Check that new interfaces are added to the TelemetryEvent union type.
|
||||
|
||||
@@ -83,6 +83,7 @@ import { useUpsertNotebookMutation } from '@/data/content/notebooks/notebook-ups
|
||||
import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql'
|
||||
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useTrack } from '@/lib/telemetry/track'
|
||||
import {
|
||||
getNotebooksStateSnapshot,
|
||||
useCurrentNotebook,
|
||||
@@ -109,6 +110,7 @@ export const ExplorerNotebookTab = () => {
|
||||
const { name, content } = currentNotebook?.notebook ?? {}
|
||||
const { isNotFound } = useLoadNotebook({ id, projectRef: ref })
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const track = useTrack()
|
||||
const cells = content?.cells ?? []
|
||||
const queryCellIds = cells.filter(isQueryCell).map((cell) => cell._id)
|
||||
|
||||
@@ -121,12 +123,31 @@ export const ExplorerNotebookTab = () => {
|
||||
} | null>(null)
|
||||
const [skipMutatingCells, setSkipMutatingCells] = useState(false)
|
||||
const [isExportingPdf, setIsExportingPdf] = useState(false)
|
||||
|
||||
const queryCellRefs = useRef(new Map<string, QueryEditorHandle>())
|
||||
const savedContentRef = useRef<typeof content>(undefined)
|
||||
const confirmedCreatedNotebookIdsRef = useRef(new Set<string>())
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { mutate: updateNotebook, isPending: isUpdating } = useUpsertNotebookMutation({
|
||||
onSuccess: (data) => {
|
||||
const { mutate: updateNotebook, isPending: isUpdating } = useUpsertNotebookMutation<{
|
||||
isCreation: boolean
|
||||
}>({
|
||||
onMutate: (variables) => ({
|
||||
isCreation:
|
||||
(currentNotebook?.status === 'new' ||
|
||||
snap.serverDivergedWhileDirty.get(variables.id) === 'deleted') &&
|
||||
!confirmedCreatedNotebookIdsRef.current.has(variables.id),
|
||||
}),
|
||||
onSuccess: (data, variables, context) => {
|
||||
const { isCreation } = context
|
||||
if (isCreation) confirmedCreatedNotebookIdsRef.current.add(variables.id)
|
||||
|
||||
track(
|
||||
isCreation ? 'explorer_notebook_created' : 'explorer_notebook_updated',
|
||||
{ notebookId: variables.id },
|
||||
{ project: variables.projectRef }
|
||||
)
|
||||
|
||||
if (id && content === savedContentRef.current) {
|
||||
snap.markSaved({ id, updatedAt: data?.updated_at })
|
||||
toast.success('Successfully saved notebook!')
|
||||
|
||||
@@ -133,6 +133,7 @@ export const ExplorerQueryTab = () => {
|
||||
<QueryEditor
|
||||
ref={queryEditorRef}
|
||||
id={id}
|
||||
location={{ surface: 'query_tab', queryId: id }}
|
||||
variant="viewport"
|
||||
title={draft.name}
|
||||
query={query}
|
||||
|
||||
@@ -118,6 +118,15 @@ export const QueryCell = forwardRef<QueryEditorHandle, QueryCellProps>(function
|
||||
<QueryEditor
|
||||
ref={ref}
|
||||
id={cell._id}
|
||||
location={
|
||||
currentNotebook
|
||||
? {
|
||||
surface: 'notebook_cell',
|
||||
notebookId: currentNotebook.notebook.id,
|
||||
cellId: cell._id,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
variant="embedded"
|
||||
className="min-h-0"
|
||||
title={title}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMonaco } from '@monaco-editor/react'
|
||||
import { acceptUntrustedSql, untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta'
|
||||
import { useFlag } from 'common'
|
||||
import type { ExplorerQueryLocation } from 'common/telemetry-constants'
|
||||
import { CodeSquare, Eye, EyeOff } from 'lucide-react'
|
||||
import type { editor as monacoEditor, Selection } from 'monaco-editor'
|
||||
import {
|
||||
@@ -73,6 +74,7 @@ import { useLatest } from '@/hooks/misc/useLatest'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { detectOS } from '@/lib/helpers'
|
||||
import { wrapWithRoleImpersonation } from '@/lib/role-impersonation'
|
||||
import { useTrack } from '@/lib/telemetry/track'
|
||||
import {
|
||||
isRoleImpersonationEnabled,
|
||||
type RoleImpersonationController,
|
||||
@@ -122,6 +124,7 @@ export type QueryEditorHandle = {
|
||||
|
||||
type QueryEditorProps = {
|
||||
id: string
|
||||
location?: ExplorerQueryLocation
|
||||
isReadOnly?: boolean
|
||||
variant: 'embedded' | 'viewport'
|
||||
title: string
|
||||
@@ -154,6 +157,7 @@ type QueryEditorProps = {
|
||||
export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(function QueryEditor(
|
||||
{
|
||||
id,
|
||||
location,
|
||||
isReadOnly = false,
|
||||
variant,
|
||||
title,
|
||||
@@ -179,6 +183,7 @@ export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(funct
|
||||
ref
|
||||
) {
|
||||
const os = detectOS()
|
||||
const track = useTrack()
|
||||
const sql = query.uncheckedSql
|
||||
const sqlRef = useLatest<string>(sql)
|
||||
const onSqlCommitRef = useLatest(onSqlCommit)
|
||||
@@ -260,13 +265,33 @@ export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(funct
|
||||
}
|
||||
|
||||
onRun?.()
|
||||
|
||||
const querySnapshot = { sql: rawSql, source: query._tag }
|
||||
const runProperties = location
|
||||
? { ...location, runId: crypto.randomUUID(), source: query._tag }
|
||||
: undefined
|
||||
const groups = { project: project.ref }
|
||||
|
||||
if (runProperties) track('explorer_query_submitted', runProperties, groups)
|
||||
|
||||
const trackRunResult = (
|
||||
failureReason?: 'logs_unavailable' | 'connection_unavailable' | 'execution_error'
|
||||
) => {
|
||||
if (!runProperties) return
|
||||
if (failureReason) {
|
||||
track('explorer_query_failed', { ...runProperties, failureReason }, groups)
|
||||
} else {
|
||||
track('explorer_query_completed', runProperties, groups)
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] This is deliberate to commit the sql, rather than the passed rawSql
|
||||
// As we want to save the cell's content into the store, rather than what's getting run
|
||||
onSqlCommit?.(sql)
|
||||
|
||||
if (query._tag === 'logs') {
|
||||
if (!isOtelLogsEnabled) {
|
||||
trackRunResult('logs_unavailable')
|
||||
onResultChange({
|
||||
error: { message: "Querying logs isn't available for this project yet." },
|
||||
...querySnapshot,
|
||||
@@ -280,12 +305,17 @@ export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(funct
|
||||
range: resolveLogTimeRange(query.time_range),
|
||||
endpoint: QUERY_SOURCE_REGISTRY.logs.endpoint,
|
||||
}).then(
|
||||
(data) =>
|
||||
(data) => {
|
||||
trackRunResult()
|
||||
onResultChange({
|
||||
rows: data.rows as readonly Record<string, unknown>[],
|
||||
...querySnapshot,
|
||||
}),
|
||||
(error) => onResultChange({ error, ...querySnapshot })
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
trackRunResult('execution_error')
|
||||
onResultChange({ error, ...querySnapshot })
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -294,6 +324,7 @@ export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(funct
|
||||
const limitedSql = applyAutoLimit(safeSql, rowLimit)
|
||||
|
||||
if (!isValidConnString(connectionString)) {
|
||||
trackRunResult('connection_unavailable')
|
||||
onResultChange({
|
||||
error: { message: 'Unable to run query: Connection string is missing' },
|
||||
...querySnapshot,
|
||||
@@ -310,8 +341,14 @@ export const QueryEditor = forwardRef<QueryEditorHandle, QueryEditorProps>(funct
|
||||
isStatementTimeoutDisabled: true,
|
||||
isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role),
|
||||
}).then(
|
||||
(data) => onResultChange({ rows: data.result, ...querySnapshot }),
|
||||
(error) => onResultChange({ error, ...querySnapshot })
|
||||
(data) => {
|
||||
trackRunResult()
|
||||
onResultChange({ rows: data.result, ...querySnapshot })
|
||||
},
|
||||
(error) => {
|
||||
trackRunResult('execution_error')
|
||||
onResultChange({ error, ...querySnapshot })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,12 @@ import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils'
|
||||
import type { Notebooks } from '@/types'
|
||||
|
||||
const testContext = vi.hoisted(() => ({
|
||||
track: vi.fn(),
|
||||
flags: { otelLegacyLogs: true } as Record<string, boolean>,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => testContext.track }))
|
||||
|
||||
vi.mock('common', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('common')>()
|
||||
return {
|
||||
@@ -119,6 +122,7 @@ const mockDatabaseQueryRequests = () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testContext.track.mockClear()
|
||||
setupSqlEditorMocks()
|
||||
testContext.flags.otelLegacyLogs = true
|
||||
for (const id of Object.keys(notebooksState.notebooks)) {
|
||||
@@ -191,6 +195,26 @@ describe('ExplorerNotebookTab', () => {
|
||||
await waitFor(() => expect(dbRequests).toHaveLength(1))
|
||||
await waitFor(() => expect(logRequests).toHaveLength(1))
|
||||
await waitFor(() => expect(runNotebookButton).toBeEnabled())
|
||||
const queryEvents = testContext.track.mock.calls.filter(([action]) =>
|
||||
action.startsWith('explorer_query_')
|
||||
)
|
||||
expect(queryEvents.filter(([action]) => action === 'explorer_query_submitted')).toHaveLength(2)
|
||||
expect(queryEvents.filter(([action]) => action === 'explorer_query_completed')).toHaveLength(2)
|
||||
for (const [, properties] of queryEvents) {
|
||||
expect(properties).toMatchObject({ surface: 'notebook_cell', notebookId: NOTEBOOK_ID })
|
||||
expect(properties.cellId).toBeDefined()
|
||||
expect(properties).not.toHaveProperty('sql')
|
||||
}
|
||||
const submittedRuns = queryEvents.filter(([action]) => action === 'explorer_query_submitted')
|
||||
const completedRuns = queryEvents.filter(([action]) => action === 'explorer_query_completed')
|
||||
expect(new Set(submittedRuns.map(([, properties]) => properties.runId)).size).toBe(2)
|
||||
for (const [, properties] of submittedRuns) {
|
||||
expect(completedRuns).toContainEqual([
|
||||
'explorer_query_completed',
|
||||
properties,
|
||||
expect.objectContaining({ project: 'default' }),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
describe('mutation confirmation on "Run notebook"', () => {
|
||||
@@ -406,6 +430,56 @@ describe('ExplorerNotebookTab', () => {
|
||||
await waitFor(() => expect(tabsState.tabsMap[tabId]?.isPreview).toBe(false))
|
||||
})
|
||||
|
||||
it('tracks confirmed creation once and later saves as updates', async () => {
|
||||
seedNotebook([databaseCell], 'new')
|
||||
addAPIMock({
|
||||
method: 'put',
|
||||
path: '/platform/projects/:ref/content',
|
||||
response: () => HttpResponse.json({ id: NOTEBOOK_ID }),
|
||||
})
|
||||
|
||||
renderNotebookTab()
|
||||
const saveButton = await screen.findByRole('button', { name: 'Save changes' })
|
||||
await userEvent.click(saveButton)
|
||||
await waitFor(() =>
|
||||
expect(testContext.track).toHaveBeenCalledWith(
|
||||
'explorer_notebook_created',
|
||||
{ notebookId: NOTEBOOK_ID },
|
||||
{ project: 'default' }
|
||||
)
|
||||
)
|
||||
|
||||
notebooksState.insertCellAfter({ id: NOTEBOOK_ID, cell: createMarkdownCellSkeleton() })
|
||||
await userEvent.click(saveButton)
|
||||
await waitFor(() =>
|
||||
expect(testContext.track).toHaveBeenCalledWith(
|
||||
'explorer_notebook_updated',
|
||||
{ notebookId: NOTEBOOK_ID },
|
||||
{ project: 'default' }
|
||||
)
|
||||
)
|
||||
expect(
|
||||
testContext.track.mock.calls.filter(([action]) => action === 'explorer_notebook_created')
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not track an unsaved draft or a failed first save', async () => {
|
||||
seedNotebook([databaseCell], 'new')
|
||||
addAPIMock({
|
||||
method: 'put',
|
||||
path: '/platform/projects/:ref/content',
|
||||
response: () => HttpResponse.json({ message: 'Save failed' }, { status: 500 }),
|
||||
})
|
||||
|
||||
renderNotebookTab()
|
||||
expect(testContext.track).not.toHaveBeenCalled()
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Save changes' }))
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: 'Save changes' })).toBeEnabled())
|
||||
expect(
|
||||
testContext.track.mock.calls.filter(([action]) => action.startsWith('explorer_notebook_'))
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('does not mark a newer edit as saved when an earlier save resolves after it', async () => {
|
||||
let resolveSave: (() => void) | undefined
|
||||
const savePromise = new Promise<void>((resolve) => {
|
||||
|
||||
@@ -15,12 +15,15 @@ import { addAPIMock } from '@/tests/lib/msw'
|
||||
import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils'
|
||||
|
||||
const testContext = vi.hoisted(() => ({
|
||||
track: vi.fn(),
|
||||
flags: { otelLegacyLogs: true } as Record<string, boolean>,
|
||||
params: { ref: 'default', id: 'query-test' } as { ref?: string; id?: string },
|
||||
/** Simulated editor selection — the mocked CodeEditor's fake editor reads this. */
|
||||
selectedText: undefined as string | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => testContext.track }))
|
||||
|
||||
vi.mock('common', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('common')>()
|
||||
return {
|
||||
@@ -218,6 +221,7 @@ const createDraft = (
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testContext.track.mockClear()
|
||||
setupSqlEditorMocks()
|
||||
testContext.flags.otelLegacyLogs = true
|
||||
testContext.params = { ref: 'default', id: 'query-test' }
|
||||
@@ -248,6 +252,20 @@ describe('QueryTab execution', () => {
|
||||
await waitFor(() => expect(explorerQueryState.results['query-test']).toBeDefined())
|
||||
expect(explorerQueryState.results['query-test']).toMatchObject({ rows: [] })
|
||||
expect(explorerQueryState.drafts['query-test']?.pendingAutoRun).toBe(false)
|
||||
const queryEvents = testContext.track.mock.calls.filter(([action]) =>
|
||||
action.startsWith('explorer_query_')
|
||||
)
|
||||
expect(queryEvents.map(([action]) => action)).toEqual([
|
||||
'explorer_query_submitted',
|
||||
'explorer_query_completed',
|
||||
])
|
||||
expect(queryEvents[0][1]).toMatchObject({
|
||||
surface: 'query_tab',
|
||||
queryId: 'query-test',
|
||||
source: 'database',
|
||||
})
|
||||
expect(queryEvents[1][1].runId).toBe(queryEvents[0][1].runId)
|
||||
expect(queryEvents[0][1]).not.toHaveProperty('sql')
|
||||
})
|
||||
|
||||
it('does not auto-run a draft created without autoRun', async () => {
|
||||
@@ -285,6 +303,53 @@ describe('QueryTab execution', () => {
|
||||
await screen.findByText("Error: Querying logs isn't available for this project yet.")
|
||||
).toBeInTheDocument()
|
||||
expect(requests).toHaveLength(0)
|
||||
expect(
|
||||
testContext.track.mock.calls.filter(([action]) => action.startsWith('explorer_query_'))
|
||||
).toEqual([
|
||||
[
|
||||
'explorer_query_submitted',
|
||||
expect.objectContaining({ source: 'logs', surface: 'query_tab' }),
|
||||
expect.objectContaining({ project: 'default' }),
|
||||
],
|
||||
[
|
||||
'explorer_query_failed',
|
||||
expect.objectContaining({ failureReason: 'logs_unavailable' }),
|
||||
expect.objectContaining({ project: 'default' }),
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('records a terminal failure when database execution rejects', async () => {
|
||||
createDraft({ _tag: 'database' })
|
||||
addAPIMock({
|
||||
method: 'post',
|
||||
path: '/platform/pg-meta/:ref/query',
|
||||
response: ({ request }) =>
|
||||
new URL(request.url).searchParams.get('key') === ''
|
||||
? HttpResponse.json({ message: 'Query failed' }, { status: 500 })
|
||||
: HttpResponse.json([]),
|
||||
})
|
||||
|
||||
renderQueryTab()
|
||||
const runButton = await screen.findByRole('button', { name: 'Run' })
|
||||
await waitFor(() => expect(runButton).toBeEnabled())
|
||||
await userEvent.click(runButton)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
testContext.track.mock.calls.filter(([action]) => action.startsWith('explorer_query_'))
|
||||
).toHaveLength(2)
|
||||
)
|
||||
const [submitted, failed] = testContext.track.mock.calls.filter(([action]) =>
|
||||
action.startsWith('explorer_query_')
|
||||
)
|
||||
expect(submitted[0]).toBe('explorer_query_submitted')
|
||||
expect(failed[0]).toBe('explorer_query_failed')
|
||||
expect(failed[1]).toMatchObject({
|
||||
runId: submitted[1].runId,
|
||||
failureReason: 'execution_error',
|
||||
})
|
||||
expect(failed[1]).not.toHaveProperty('message')
|
||||
})
|
||||
|
||||
it('exposes and persists the shared notebook result view options', async () => {
|
||||
@@ -443,6 +508,9 @@ describe('QueryTab execution', () => {
|
||||
|
||||
expect(await screen.findByText('Potential issue detected')).toBeInTheDocument()
|
||||
expect(executedQueries).toHaveLength(0)
|
||||
expect(
|
||||
testContext.track.mock.calls.filter(([action]) => action.startsWith('explorer_query_'))
|
||||
).toEqual([])
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Run query' }))
|
||||
|
||||
|
||||
@@ -69,17 +69,17 @@ export async function upsertNotebook(
|
||||
|
||||
export type UpdateNotebookData = Awaited<ReturnType<typeof upsertNotebook>>
|
||||
|
||||
export const useUpsertNotebookMutation = ({
|
||||
export const useUpsertNotebookMutation = <TContext = unknown>({
|
||||
onError,
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseCustomMutationOptions<UpdateNotebookData, ResponseError, UpsertNotebookVariables>,
|
||||
UseCustomMutationOptions<UpdateNotebookData, ResponseError, UpsertNotebookVariables, TContext>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<UpdateNotebookData, ResponseError, UpsertNotebookVariables>({
|
||||
return useMutation<UpdateNotebookData, ResponseError, UpsertNotebookVariables, TContext>({
|
||||
mutationFn: (args) => upsertNotebook(args),
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef, id } = variables
|
||||
|
||||
@@ -3459,6 +3459,85 @@ export interface LogExplorerQueryRunButtonClickedEvent {
|
||||
groups: TelemetryGroups
|
||||
}
|
||||
|
||||
export type ExplorerQueryLocation =
|
||||
| { surface: 'query_tab'; queryId: string; notebookId?: never; cellId?: never }
|
||||
| { surface: 'notebook_cell'; notebookId: string; cellId: string; queryId?: never }
|
||||
|
||||
export type ExplorerQueryRunProperties = ExplorerQueryLocation & {
|
||||
runId: string
|
||||
source: 'database' | 'logs'
|
||||
}
|
||||
|
||||
type ExplorerGroups = Pick<TelemetryGroups, 'project'> &
|
||||
Partial<Pick<TelemetryGroups, 'organization'>>
|
||||
|
||||
/**
|
||||
* User started an Explorer query run.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}/explorer
|
||||
*/
|
||||
export interface ExplorerQuerySubmittedEvent {
|
||||
action: 'explorer_query_submitted'
|
||||
properties: ExplorerQueryRunProperties
|
||||
groups: ExplorerGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* An Explorer query run completed successfully.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}/explorer
|
||||
*/
|
||||
export interface ExplorerQueryCompletedEvent {
|
||||
action: 'explorer_query_completed'
|
||||
properties: ExplorerQueryRunProperties
|
||||
groups: ExplorerGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* An Explorer query run failed.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}/explorer
|
||||
*/
|
||||
export interface ExplorerQueryFailedEvent {
|
||||
action: 'explorer_query_failed'
|
||||
properties: ExplorerQueryRunProperties & {
|
||||
failureReason: 'logs_unavailable' | 'connection_unavailable' | 'execution_error'
|
||||
}
|
||||
groups: ExplorerGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* An Explorer notebook was saved for the first time.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}/explorer
|
||||
*/
|
||||
export interface ExplorerNotebookCreatedEvent {
|
||||
action: 'explorer_notebook_created'
|
||||
properties: { notebookId: string }
|
||||
groups: ExplorerGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* An Explorer notebook was saved after creation.
|
||||
*
|
||||
* @group Events
|
||||
* @source studio
|
||||
* @page /project/{ref}/explorer
|
||||
*/
|
||||
export interface ExplorerNotebookUpdatedEvent {
|
||||
action: 'explorer_notebook_updated'
|
||||
properties: { notebookId: string }
|
||||
groups: ExplorerGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked an upgrade CTA inside the compute badge hover card.
|
||||
*
|
||||
@@ -3981,6 +4060,11 @@ export interface WarehouseDisabledEvent {
|
||||
* @hidden
|
||||
*/
|
||||
export type TelemetryEvent =
|
||||
| ExplorerQuerySubmittedEvent
|
||||
| ExplorerQueryCompletedEvent
|
||||
| ExplorerQueryFailedEvent
|
||||
| ExplorerNotebookCreatedEvent
|
||||
| ExplorerNotebookUpdatedEvent
|
||||
| SignUpEvent
|
||||
| SignInEvent
|
||||
| SignInSubmittedEvent
|
||||
|
||||
@@ -430,6 +430,8 @@ export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathn
|
||||
groups: 'groups' in event ? { ...event.groups } : {},
|
||||
}
|
||||
|
||||
if (event.action.startsWith('explorer_')) body.page_title = 'Explorer'
|
||||
|
||||
if (body.groups?.project === 'Unknown') {
|
||||
delete body.groups.project
|
||||
if (body.groups?.organization === 'Unknown') {
|
||||
|
||||
Reference in new issue
Block a user