Files
supabase/apps/studio/data/content/notebooks/notebook-upsert-mutation.ts
Pamela ChiaandJoshen Lim 4a941518e3 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>
2026-09-29 13:54:13 -07:00

103 lines
2.9 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { upsertContent, type UpsertContentPayload } from '../content-upsert-mutation'
import { contentKeys } from '../keys'
import { writableNotebookSchema, type WritableNotebook } from './notebook-schema'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
function buildNotebookUpsertPayload({
id,
name,
description,
content,
}: {
id: string
name: string
description?: string
content: WritableNotebook
}): UpsertContentPayload {
return {
id,
name,
description,
type: 'notebook',
visibility: 'project',
content: writableNotebookSchema.parse(content),
}
}
export type CreateNotebookVariables = {
projectRef: string
name: string
description?: string
content: WritableNotebook
}
export async function createNotebook(
{ projectRef, name, description, content }: CreateNotebookVariables,
signal?: AbortSignal,
headersInit?: HeadersInit
) {
const id = crypto.randomUUID()
const payload = buildNotebookUpsertPayload({ id, name, description, content })
await upsertContent({ projectRef, payload }, signal, headersInit)
return { id }
}
export type CreateNotebookData = Awaited<ReturnType<typeof createNotebook>>
export type UpsertNotebookVariables = {
id: string
projectRef: string
name: string
description?: string
content: WritableNotebook
}
export async function upsertNotebook(
{ projectRef, id, name, description, content }: UpsertNotebookVariables,
signal?: AbortSignal,
headersInit?: HeadersInit
) {
const payload = buildNotebookUpsertPayload({ id, name, description, content })
return upsertContent({ projectRef, payload }, signal, headersInit)
}
export type UpdateNotebookData = Awaited<ReturnType<typeof upsertNotebook>>
export const useUpsertNotebookMutation = <TContext = unknown>({
onError,
onSuccess,
...options
}: Omit<
UseCustomMutationOptions<UpdateNotebookData, ResponseError, UpsertNotebookVariables, TContext>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<UpdateNotebookData, ResponseError, UpsertNotebookVariables, TContext>({
mutationFn: (args) => upsertNotebook(args),
async onSuccess(data, variables, context) {
const { projectRef, id } = variables
await Promise.all([
queryClient.invalidateQueries({ queryKey: contentKeys.allContentLists(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.infiniteList(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.resource(projectRef, id) }),
])
await onSuccess?.(data, variables, context)
},
async onError(error, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update notebook: ${error.message}`)
} else {
onError(error, variables, context)
}
},
...options,
})
}