Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/search.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
sparse-checkout: |
apps/docs
apps/www/.env.local.example
examples
supabase

- uses: pnpm/action-setup@v4
Expand All @@ -63,7 +64,9 @@ jobs:
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
run: pnpm run embeddings
run: |
pnpm run codegen:examples
pnpm run embeddings

- name: Refresh embeddings
working-directory: ./apps/docs
Expand Down
39 changes: 39 additions & 0 deletions apps/docs/content/guides/deployment/branching/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ supabase --experimental branches create --persistent
# Do you want to create a branch named develop? [Y/n]
```

<Admonition type="tip">

To retrieve the project ID for an existing branch, use the `branches list` command:

```bash
supabase --experimental branches list
```

This will display a table showing all your branches with their corresponding project ID.
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.

</Admonition>

### Configuration merging

When merging a PR into a persistent branch, the Supabase integration:
Expand Down Expand Up @@ -186,6 +199,19 @@ max_rows = 500
pool_size = 25
```

<Admonition type="tip">

To retrieve the project ID for an existing branch, use the `branches list` command:

```bash
supabase --experimental branches list
```

This will display a table showing all your branches with their corresponding project ID.
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.

</Admonition>

### Feature branch configuration

For feature branches that need specific settings:
Expand All @@ -200,6 +226,19 @@ client_id = "env(GOOGLE_CLIENT_ID)"
secret = "env(GOOGLE_CLIENT_SECRET)"
```

<Admonition type="tip">

To retrieve the project ID for an existing branch, use the `branches list` command:

```bash
supabase --experimental branches list
```

This will display a table showing all your branches with their corresponding project ID.
Use the value from the `BRANCH PROJECT ID` column as your `project_id` in the remote configuration.

</Admonition>

## Next steps

- Explore [branching integrations](/docs/guides/deployment/branching/integrations)
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/features/docs/GuidesMdx.template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
)}
<hr className="not-prose border-t-0 border-b my-8" />

{content && <MDXRemoteBase source={content} options={mdxOptions} />}
{content && (
<MDXRemoteBase source={content} options={mdxOptions} customPreprocess={(x) => x} />
)}
{children}

<footer className="mt-16 not-prose">
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/features/docs/GuidesMdx.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => {
editLink,
}
} catch (error: unknown) {
if (error instanceof FileNotFoundError) {
if (error instanceof Error && error.cause instanceof FileNotFoundError) {
// Not using console.error because this includes pages that are genuine
// 404s and clutters up the logs
console.log('Could not read Markdown at path: %s', fullPath)
Expand Down
83 changes: 72 additions & 11 deletions packages/common/posthog-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import posthog from 'posthog-js'
import { PostHogConfig } from 'posthog-js'

// Limit the max number of queued events
// (e.g. if a user navigates around a lot before accepting consent)
const MAX_PENDING_EVENTS = 20

interface PostHogClientConfig {
apiKey?: string
apiHost?: string
Expand All @@ -10,7 +14,9 @@ class PostHogClient {
private initialized = false
private pendingGroups: Record<string, string> = {}
private pendingIdentification: { userId: string; properties?: Record<string, any> } | null = null
private pendingEvents: Array<{ event: string; properties: Record<string, any> }> = []
private config: PostHogClientConfig
private readonly maxPendingEvents = MAX_PENDING_EVENTS

constructor(config: PostHogClientConfig = {}) {
this.config = {
Expand All @@ -34,6 +40,10 @@ class PostHogClient {
capture_pageview: false, // We'll manually track pageviews
capture_pageleave: false, // We'll manually track page leaves
loaded: (posthog) => {
// Apply pending properties that were set before PostHog
// initialized due to poor connection or user not accepting
// consent right away

// Apply any pending groups
Object.entries(this.pendingGroups).forEach(([type, id]) => {
posthog.group(type, id)
Expand All @@ -42,9 +52,26 @@ class PostHogClient {

// Apply any pending identification
if (this.pendingIdentification) {
posthog.identify(this.pendingIdentification.userId, this.pendingIdentification.properties)
try {
posthog.identify(
this.pendingIdentification.userId,
this.pendingIdentification.properties
)
} catch (error) {
console.error('PostHog identify failed:', error)
}
this.pendingIdentification = null
}

// Flush any pending events
this.pendingEvents.forEach(({ event, properties }) => {
try {
posthog.capture(event, properties, { transport: 'sendBeacon' })
} catch (error) {
console.error('PostHog capture failed:', error)
}
})
this.pendingEvents = []
},
}

Expand All @@ -53,21 +80,51 @@ class PostHogClient {
}

capturePageView(properties: Record<string, any>, hasConsent: boolean = true) {
if (!hasConsent || !this.initialized) return
if (!hasConsent) return

// Store groups from properties if present (for later group() calls)
if (properties.$groups) {
Object.entries(properties.$groups).forEach(([type, id]) => {
if (id) posthog.group(type, id as string)
})
if (!this.initialized) {
// Queue the event for when PostHog initializes (up to cap)
// (e.g. poor connection or user not accepting consent right away)
if (this.pendingEvents.length >= this.maxPendingEvents) {
this.pendingEvents.shift() // Remove oldest event
}
this.pendingEvents.push({ event: '$pageview', properties })
return
}

posthog.capture('$pageview', properties)
try {
// Store groups from properties if present (for later group() calls)
if (properties.$groups) {
Object.entries(properties.$groups).forEach(([type, id]) => {
if (id) posthog.group(type, id as string)
})
}

posthog.capture('$pageview', properties, { transport: 'sendBeacon' })
} catch (error) {
console.error('PostHog pageview capture failed:', error)
}
}

capturePageLeave(properties: Record<string, any>, hasConsent: boolean = true) {
if (!hasConsent || !this.initialized) return
posthog.capture('$pageleave', properties)
if (!hasConsent) return

if (!this.initialized) {
// Queue the event for when PostHog initializes (up to cap)
// (e.g. poor connection or user not accepting consent right away)
if (this.pendingEvents.length >= this.maxPendingEvents) {
this.pendingEvents.shift() // Remove oldest event
}
this.pendingEvents.push({ event: '$pageleave', properties })
return
}

try {
// Use sendBeacon for page leave to survive tab close
posthog.capture('$pageleave', properties, { transport: 'sendBeacon' })
} catch (error) {
console.error('PostHog pageleave capture failed:', error)
}
}

identify(userId: string, properties?: Record<string, any>, hasConsent: boolean = true) {
Expand All @@ -79,7 +136,11 @@ class PostHogClient {
return
}

posthog.identify(userId, properties)
try {
posthog.identify(userId, properties)
} catch (error) {
console.error('PostHog identify failed:', error)
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions packages/common/telemetry-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import { isBrowser } from './helpers'

// Parse session_id from PostHog cookie since SDK doesn't expose session ID
// (needed to correlate client and server events)
function getPostHogSessionId(): string | null {
if (!isBrowser) return null

try {
// Parse PostHog cookie to extract session ID
const phCookies = document.cookie.split(';').find((cookie) => cookie.trim().startsWith('ph_'))

if (phCookies) {
const cookieValue = decodeURIComponent(phCookies.split('=')[1])
const phData = JSON.parse(cookieValue)
if (phData.$sesid && Array.isArray(phData.$sesid) && phData.$sesid[1]) {
return phData.$sesid[1]
}
}
} catch (error) {
console.warn('Could not extract PostHog session ID:', error)
}

return null
}

export function getSharedTelemetryData(pathname?: string) {
const sessionId = getPostHogSessionId()

return {
page_url: isBrowser ? window.location.href : '',
page_title: isBrowser ? document?.title : '',
pathname: pathname ? pathname : isBrowser ? window.location.pathname : '',
session_id: sessionId,
ph: {
referrer: isBrowser ? document?.referrer : '',
language: navigator.language ?? 'en-US',
Expand Down
23 changes: 17 additions & 6 deletions packages/common/telemetry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export function handlePageTelemetry(
// Send to PostHog client-side (only in browser)
if (typeof window !== 'undefined') {
const pageData = getSharedTelemetryData(pathname)

// Align frontend and backend session IDs for correlation
if (pageData.session_id) {
document.cookie = `session_id=${pageData.session_id}; path=/; SameSite=Lax`
}

posthogClient.capturePageView({
$current_url: pageData.page_url,
$pathname: pageData.pathname,
Expand All @@ -63,6 +69,7 @@ export function handlePageTelemetry(
...(ref ? { project: ref } : {}),
},
page_title: pageData.page_title,
...(pageData.session_id && { $session_id: pageData.session_id }),
...pageData.ph,
...Object.fromEntries(
Object.entries(featureFlags || {}).map(([k, v]) => [`$feature/${k}`, v])
Expand All @@ -72,12 +79,14 @@ export function handlePageTelemetry(

// Send to backend
// TODO: Remove this once migration to client-side page telemetry is complete
return post(
`${ensurePlatformSuffix(API_URL)}/telemetry/page`,
const sharedData = getSharedTelemetryData(pathname)
const { session_id, ...backendData } = sharedData // Remove session_id from backend payload (it's already in the cookie)

const payload =
telemetryDataOverride !== undefined
? { feature_flags: featureFlags, ...telemetryDataOverride }
: {
...getSharedTelemetryData(pathname),
...backendData,
...(slug || ref
? {
groups: {
Expand All @@ -87,9 +96,10 @@ export function handlePageTelemetry(
}
: {}),
feature_flags: featureFlags,
},
{ headers: { Version: '2' } }
)
}
return post(`${ensurePlatformSuffix(API_URL)}/telemetry/page`, payload, {
headers: { Version: '2' },
})
}

export function handlePageLeaveTelemetry(
Expand All @@ -108,6 +118,7 @@ export function handlePageLeaveTelemetry(
$current_url: pageData.page_url,
$pathname: pageData.pathname,
page_title: pageData.page_title,
...(pageData.session_id && { $session_id: pageData.session_id }),
})
}

Expand Down
Loading