Generate a password
+ .
>
)
diff --git a/apps/studio/data/analytics/functions-req-stats-query.ts b/apps/studio/data/analytics/functions-req-stats-query.ts
index d386a07b465..5a765b4148f 100644
--- a/apps/studio/data/analytics/functions-req-stats-query.ts
+++ b/apps/studio/data/analytics/functions-req-stats-query.ts
@@ -6,7 +6,7 @@ import { analyticsKeys } from './keys'
export type FunctionsReqStatsVariables = {
projectRef?: string
functionId?: string
- interval?: operations['FunctionRequestLogsController_getStatus']['parameters']['query']['interval']
+ interval?: operations['FunctionsLogsController_getRequestStats']['parameters']['query']['interval']
}
export type FunctionsReqStatsResponse = any
diff --git a/apps/studio/data/analytics/functions-resource-usage-query.ts b/apps/studio/data/analytics/functions-resource-usage-query.ts
index cbb71f1d78e..ae767d52ebb 100644
--- a/apps/studio/data/analytics/functions-resource-usage-query.ts
+++ b/apps/studio/data/analytics/functions-resource-usage-query.ts
@@ -6,7 +6,7 @@ import { analyticsKeys } from './keys'
export type FunctionsResourceUsageVariables = {
projectRef?: string
functionId?: string
- interval?: operations['FunctionResourceLogsController_getStatus']['parameters']['query']['interval']
+ interval?: operations['FunctionsLogsController_getRequestStats']['parameters']['query']['interval']
}
export type FunctionsResourceUsageResponse = any
diff --git a/apps/studio/data/content/content-delete-mutation.ts b/apps/studio/data/content/content-delete-mutation.ts
index d5fd129fa3f..a8110e1e8f6 100644
--- a/apps/studio/data/content/content-delete-mutation.ts
+++ b/apps/studio/data/content/content-delete-mutation.ts
@@ -15,7 +15,7 @@ export async function deleteContents(
headers: { Version: '2' },
params: {
path: { ref: projectRef },
- query: { ids },
+ query: { ids: ids.join(',') },
},
signal,
})
diff --git a/apps/studio/data/edge-functions/edge-function-body-query.ts b/apps/studio/data/edge-functions/edge-function-body-query.ts
index d604b96395f..ddd91121faf 100644
--- a/apps/studio/data/edge-functions/edge-function-body-query.ts
+++ b/apps/studio/data/edge-functions/edge-function-body-query.ts
@@ -15,6 +15,7 @@ export type EdgeFunctionFile = {
}
export type EdgeFunctionBodyResponse = {
+ version: number
files: EdgeFunctionFile[]
}
@@ -51,11 +52,14 @@ export async function getEdgeFunctionBody(
)
}
- const { files } = await parseResponse.json()
- return files as EdgeFunctionFile[]
+ const response = (await parseResponse.json()) as EdgeFunctionBodyResponse
+ return response
} catch (error) {
handleError(error)
- return []
+ return {
+ version: 0,
+ files: [],
+ } as EdgeFunctionBodyResponse
}
}
diff --git a/apps/studio/data/lint/delete-lint-rule-mutation.ts b/apps/studio/data/lint/delete-lint-rule-mutation.ts
index 70dca1fdd73..cf6538fdff5 100644
--- a/apps/studio/data/lint/delete-lint-rule-mutation.ts
+++ b/apps/studio/data/lint/delete-lint-rule-mutation.ts
@@ -12,7 +12,7 @@ export type LintRuleDeleteVariables = {
export async function deleteLintRule({ projectRef, ids }: LintRuleDeleteVariables) {
const { data, error } = await del('/platform/projects/{ref}/notifications/advisor/exceptions', {
- params: { path: { ref: projectRef }, query: { ids } },
+ params: { path: { ref: projectRef }, query: { ids: ids.join(',') } },
})
if (error) handleError(error)
diff --git a/apps/studio/data/organizations/organization-available-regions-query.ts b/apps/studio/data/organizations/organization-available-regions-query.ts
index 93eed9a4f0e..01bcd9af28f 100644
--- a/apps/studio/data/organizations/organization-available-regions-query.ts
+++ b/apps/studio/data/organizations/organization-available-regions-query.ts
@@ -6,7 +6,7 @@ import { organizationKeys } from './keys'
export type OrganizationAvailableRegionsVariables = {
slug?: string
- cloudProvider: 'AWS' | 'FLY' | 'AWS_K8S'
+ cloudProvider: 'AWS' | 'FLY' | 'AWS_K8S' | 'AWS_NIMBUS'
}
export async function getOrganizationAvailableRegions(
diff --git a/apps/studio/data/projects/new-project.constants.ts b/apps/studio/data/projects/new-project.constants.ts
index 58f55cf9faa..accac2b6757 100644
--- a/apps/studio/data/projects/new-project.constants.ts
+++ b/apps/studio/data/projects/new-project.constants.ts
@@ -31,7 +31,12 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.01344,
priceMonthly: 10,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
small: {
label: 'Small',
@@ -39,7 +44,12 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.0206,
priceMonthly: 15,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
medium: {
label: 'Medium',
@@ -47,7 +57,12 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.0822,
priceMonthly: 60,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
large: {
label: 'Large',
@@ -55,7 +70,12 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.1517,
priceMonthly: 110,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
xlarge: {
label: 'XL',
@@ -63,7 +83,12 @@ export const instanceSizeSpecs: Record<
cpu: '4-core',
priceHourly: 0.2877,
priceMonthly: 210,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
'2xlarge': {
label: '2XL',
@@ -71,7 +96,12 @@ export const instanceSizeSpecs: Record<
cpu: '8-core',
priceHourly: 0.562,
priceMonthly: 410,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
'4xlarge': {
label: '4XL',
@@ -79,7 +109,12 @@ export const instanceSizeSpecs: Record<
cpu: '16-core',
priceHourly: 1.32,
priceMonthly: 960,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.FLY.id],
+ cloud_providers: [
+ PROVIDERS.AWS.id,
+ PROVIDERS.AWS_K8S.id,
+ PROVIDERS.AWS_NIMBUS.id,
+ PROVIDERS.FLY.id,
+ ],
},
'8xlarge': {
label: '8XL',
@@ -87,7 +122,7 @@ export const instanceSizeSpecs: Record<
cpu: '32-core',
priceHourly: 2.562,
priceMonthly: 1870,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'12xlarge': {
label: '12XL',
@@ -95,7 +130,7 @@ export const instanceSizeSpecs: Record<
cpu: '48-core',
priceHourly: 3.836,
priceMonthly: 2800,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'16xlarge': {
label: '16XL',
@@ -103,7 +138,7 @@ export const instanceSizeSpecs: Record<
cpu: '64-core',
priceHourly: 5.12,
priceMonthly: 3730,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'24xlarge': {
label: '24XL',
@@ -111,7 +146,7 @@ export const instanceSizeSpecs: Record<
cpu: '96-core',
priceHourly: 9.73,
priceMonthly: 7100,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'24xlarge_optimized_cpu': {
label: '24XL - Optimized CPU',
@@ -119,7 +154,7 @@ export const instanceSizeSpecs: Record<
cpu: '96-core',
priceHourly: 8.9,
priceMonthly: 6500,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'24xlarge_optimized_memory': {
label: '24XL - Optimized Memory',
@@ -127,7 +162,7 @@ export const instanceSizeSpecs: Record<
cpu: '96-core',
priceHourly: 13.84,
priceMonthly: 10100,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'24xlarge_high_memory': {
label: '24XL - High Memory',
@@ -135,7 +170,7 @@ export const instanceSizeSpecs: Record<
cpu: '96-core',
priceHourly: 21.91,
priceMonthly: 16000,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'48xlarge': {
label: '48XL',
@@ -143,7 +178,7 @@ export const instanceSizeSpecs: Record<
cpu: '192-core',
priceHourly: 19.47,
priceMonthly: 14200,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'48xlarge_optimized_cpu': {
label: '48XL - Optimized CPU',
@@ -151,7 +186,7 @@ export const instanceSizeSpecs: Record<
cpu: '192-core',
priceHourly: 17.8,
priceMonthly: 13000,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'48xlarge_optimized_memory': {
label: '48XL - Optimized Memory',
@@ -159,7 +194,7 @@ export const instanceSizeSpecs: Record<
cpu: '192-core',
priceHourly: 27.68,
priceMonthly: 20200,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
'48xlarge_high_memory': {
label: '48XL - High Memory',
@@ -167,6 +202,6 @@ export const instanceSizeSpecs: Record<
cpu: '192-core',
priceHourly: 43.84,
priceMonthly: 32000,
- cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id],
+ cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_K8S.id, PROVIDERS.AWS_NIMBUS.id],
},
}
diff --git a/apps/studio/data/replication/create-destination-pipeline-mutation.ts b/apps/studio/data/replication/create-destination-pipeline-mutation.ts
index 67866a6f18b..ce9e285b092 100644
--- a/apps/studio/data/replication/create-destination-pipeline-mutation.ts
+++ b/apps/studio/data/replication/create-destination-pipeline-mutation.ts
@@ -1,15 +1,15 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
+import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { replicationKeys } from './keys'
-import { handleError, post } from 'data/fetchers'
export type BigQueryDestinationConfig = {
projectId: string
datasetId: string
serviceAccountKey: string
- maxStalenessMins: number
+ maxStalenessMins?: number
}
export type CreateDestinationPipelineParams = {
@@ -21,7 +21,7 @@ export type CreateDestinationPipelineParams = {
sourceId: number
pipelineConfig: {
publicationName: string
- batch: {
+ batch?: {
maxSize: number
maxFillMs: number
}
@@ -35,10 +35,7 @@ async function createDestinationPipeline(
destinationConfig: {
bigQuery: { projectId, datasetId, serviceAccountKey, maxStalenessMins },
},
- pipelineConfig: {
- publicationName,
- batch: { maxSize, maxFillMs },
- },
+ pipelineConfig: { publicationName, batch },
sourceId,
}: CreateDestinationPipelineParams,
signal?: AbortSignal
@@ -48,23 +45,27 @@ async function createDestinationPipeline(
const { data, error } = await post('/platform/replication/{ref}/destinations-pipelines', {
params: { path: { ref: projectRef } },
body: {
+ source_id: sourceId,
destination_name: destinationName,
destination_config: {
big_query: {
project_id: projectId,
dataset_id: datasetId,
service_account_key: serviceAccountKey,
- max_staleness_mins: maxStalenessMins,
+ ...(maxStalenessMins != null && { max_staleness_mins: maxStalenessMins }),
},
},
pipeline_config: {
publication_name: publicationName,
- batch: {
- max_size: maxSize,
- max_fill_ms: maxFillMs,
- },
+ ...(batch
+ ? {
+ batch: {
+ max_size: batch.maxSize,
+ max_fill_ms: batch.maxFillMs,
+ },
+ }
+ : {}),
},
- source_id: sourceId,
},
signal,
})
@@ -92,8 +93,12 @@ export const useCreateDestinationPipelineMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
- await queryClient.invalidateQueries(replicationKeys.destinations(projectRef))
- await queryClient.invalidateQueries(replicationKeys.pipelines(projectRef))
+
+ await Promise.all([
+ queryClient.invalidateQueries(replicationKeys.destinations(projectRef)),
+ queryClient.invalidateQueries(replicationKeys.pipelines(projectRef)),
+ ])
+
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/replication/delete-destination-pipeline-mutation.ts b/apps/studio/data/replication/delete-destination-pipeline-mutation.ts
index 2ccb2473423..91766e5a80d 100644
--- a/apps/studio/data/replication/delete-destination-pipeline-mutation.ts
+++ b/apps/studio/data/replication/delete-destination-pipeline-mutation.ts
@@ -1,9 +1,9 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
+import { del, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { replicationKeys } from './keys'
-import { handleError, del } from 'data/fetchers'
export type DeleteDestinationPipelineParams = {
projectRef: string
@@ -47,8 +47,19 @@ export const useDeleteDestinationPipelineMutation = ({
(vars) => deleteDestinationPipeline(vars),
{
async onSuccess(data, variables, context) {
- const { projectRef } = variables
- await queryClient.invalidateQueries(replicationKeys.destinations(projectRef))
+ const { projectRef, destinationId, pipelineId } = variables
+
+ await Promise.all([
+ queryClient.invalidateQueries(replicationKeys.destinations(projectRef)),
+ queryClient.invalidateQueries(replicationKeys.pipelines(projectRef)),
+ queryClient.invalidateQueries(replicationKeys.pipelineById(projectRef, pipelineId)),
+ queryClient.invalidateQueries(replicationKeys.pipelinesStatus(projectRef, pipelineId)),
+ queryClient.invalidateQueries(
+ replicationKeys.pipelinesReplicationStatus(projectRef, pipelineId)
+ ),
+ queryClient.invalidateQueries(replicationKeys.destinationById(projectRef, destinationId)),
+ ])
+
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/replication/update-destination-pipeline-mutation.ts b/apps/studio/data/replication/update-destination-pipeline-mutation.ts
index ee0cd6539ce..2c04b48db4c 100644
--- a/apps/studio/data/replication/update-destination-pipeline-mutation.ts
+++ b/apps/studio/data/replication/update-destination-pipeline-mutation.ts
@@ -1,15 +1,15 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
+import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { replicationKeys } from './keys'
-import { handleError, post } from 'data/fetchers'
export type BigQueryDestinationConfig = {
projectId: string
datasetId: string
serviceAccountKey: string
- maxStalenessMins: number
+ maxStalenessMins?: number
}
export type UpdateDestinationPipelineParams = {
@@ -23,7 +23,7 @@ export type UpdateDestinationPipelineParams = {
sourceId: number
pipelineConfig: {
publicationName: string
- batch: {
+ batch?: {
maxSize: number
maxFillMs: number
}
@@ -39,10 +39,7 @@ async function updateDestinationPipeline(
destinationConfig: {
bigQuery: { projectId, datasetId, serviceAccountKey, maxStalenessMins },
},
- pipelineConfig: {
- publicationName,
- batch: { maxSize, maxFillMs },
- },
+ pipelineConfig: { publicationName, batch },
sourceId,
}: UpdateDestinationPipelineParams,
signal?: AbortSignal
@@ -60,15 +57,17 @@ async function updateDestinationPipeline(
project_id: projectId,
dataset_id: datasetId,
service_account_key: serviceAccountKey,
- max_staleness_mins: maxStalenessMins,
+ ...(maxStalenessMins != null && { max_staleness_mins: maxStalenessMins }),
},
},
pipeline_config: {
publication_name: publicationName,
- batch: {
- max_size: maxSize,
- max_fill_ms: maxFillMs,
- },
+ ...(batch && {
+ batch: {
+ max_size: batch.maxSize,
+ max_fill_ms: batch.maxFillMs,
+ },
+ }),
},
source_id: sourceId,
},
@@ -99,8 +98,12 @@ export const useUpdateDestinationPipelineMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
- await queryClient.invalidateQueries(replicationKeys.destinations(projectRef))
- await queryClient.invalidateQueries(replicationKeys.pipelines(projectRef))
+
+ await Promise.all([
+ queryClient.invalidateQueries(replicationKeys.destinations(projectRef)),
+ queryClient.invalidateQueries(replicationKeys.pipelines(projectRef)),
+ ])
+
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/reports/edgefn-charts.ts b/apps/studio/data/reports/edgefn-charts.ts
deleted file mode 100644
index dbcd204e53d..00000000000
--- a/apps/studio/data/reports/edgefn-charts.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
-
-export const getEdgeFunctionReportAttributes = (): ReportAttributes[] => [
- {
- id: 'execution-status-codes',
- label: 'Edge Function Status Codes',
- valuePrecision: 0,
- hide: false,
- showTooltip: true,
- showLegend: true,
- showMaxValue: false,
- hideChartType: false,
- defaultChartStyle: 'bar',
- titleTooltip: 'The total number of edge function executions by status code.',
- availableIn: ['free', 'pro', 'team', 'enterprise'],
- attributes: [
- {
- attribute: 'ExecutionStatusCodes',
- provider: 'logs',
- label: 'Execution Status Codes',
- },
- ],
- },
- {
- id: 'execution-time',
- label: 'Edge Function Execution Time',
- valuePrecision: 0,
- hide: false,
- showTooltip: true,
- showLegend: true,
- showMaxValue: false,
- hideChartType: false,
- defaultChartStyle: 'line',
- titleTooltip: 'Average execution time for edge functions.',
- availableIn: ['free', 'pro', 'team', 'enterprise'],
- format: 'ms',
- YAxisProps: {
- width: 50,
- tickFormatter: (value: number) => `${value}ms`,
- },
- attributes: [
- {
- attribute: 'ExecutionTime',
- label: 'Avg. Execution Time (ms)',
- provider: 'logs',
- enabled: true,
- },
- ],
- },
- {
- id: 'invocations-by-region',
- label: 'Edge Function Invocations by Region',
- valuePrecision: 0,
- hide: false,
- showTooltip: true,
- showLegend: true,
- showMaxValue: false,
- hideChartType: false,
- defaultChartStyle: 'bar',
- titleTooltip: 'The total number of edge function invocations by region.',
- availableIn: ['pro', 'team', 'enterprise'],
- attributes: [
- {
- attribute: 'InvocationsByRegion',
- provider: 'logs',
- label: 'Invocations by Region',
- },
- ],
- },
-]
diff --git a/apps/studio/data/reports/edgefn-query.ts b/apps/studio/data/reports/edgefn-query.ts
deleted file mode 100644
index 1671f7e4ec4..00000000000
--- a/apps/studio/data/reports/edgefn-query.ts
+++ /dev/null
@@ -1,415 +0,0 @@
-import { useQuery } from '@tanstack/react-query'
-import { get } from 'data/fetchers'
-import { AnalyticsInterval } from 'data/analytics/constants'
-import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
-import { getHttpStatusCodeInfo } from 'lib/http-status-codes'
-import { analyticsIntervalToGranularity, useEdgeFnIdToName } from './report.utils'
-import { REPORT_STATUS_CODE_COLORS } from './report.utils'
-
-/**
- * METRICS
- * Each chart in the UI has a corresponding metric key.
- */
-
-const METRIC_KEYS = [
- 'TotalInvocations',
- 'ExecutionStatusCodes',
- 'InvocationsByRegion',
- 'ExecutionTime',
-]
-
-type MetricKey = (typeof METRIC_KEYS)[number]
-
-/**
- * SQL
- * Each metric has a corresponding SQL query.
- */
-
-const METRIC_SQL: Record<
- MetricKey,
- (interval: AnalyticsInterval, functionIds?: string[]) => string
-> = {
- TotalInvocations: (interval, functionIds) => {
- return `
---edgefn-report-invocations
-select
- timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
- function_id,
- count(*) as count
-from
- function_edge_logs
- CROSS JOIN UNNEST(metadata) AS m
- CROSS JOIN UNNEST(m.request) AS request
- CROSS JOIN UNNEST(m.response) AS response
- ${
- functionIds && functionIds.length > 0
- ? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
- : ''
- }
-group by
- timestamp,
- function_id
-order by
- timestamp desc;
-
- `
- },
- ExecutionStatusCodes: (interval, functionIds) => {
- return `
---edgefn-report-execution-status-codes
-select
- timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
- response.status_code AS status_code,
- COUNT(*) AS count
-FROM
- function_edge_logs
- CROSS JOIN UNNEST(metadata) AS m
- CROSS JOIN UNNEST(m.response) AS response
- CROSS JOIN UNNEST(m.request) AS request
- ${
- functionIds && functionIds.length > 0
- ? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
- : ''
- }
-group by
- timestamp,
- status_code
-order by
- timestamp desc;
- `
- },
- InvocationsByRegion: (interval, functionIds) => {
- const granularity = analyticsIntervalToGranularity(interval)
- return `
---edgefn-report-invocations-by-region
-select
- timestamp_trunc(timestamp, ${granularity}) as timestamp,
- h.x_sb_edge_region as region,
- count(*) as count
-from
- function_edge_logs
- cross join unnest(metadata) as m
- cross join unnest(m.response) as r
- cross join unnest(r.headers) as h
- where h.x_sb_edge_region is not null
- ${
- functionIds && functionIds.length > 0
- ? `and function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
- : ''
- }
-group by
- timestamp,
- region
-order by
- timestamp desc
- `
- },
- ExecutionTime: (interval, functionIds) => {
- const granularity = analyticsIntervalToGranularity(interval)
- const hasFunctions = functionIds && functionIds.length > 0
- return `
---edgefn-report-execution-time
-select
- timestamp_trunc(timestamp, ${granularity}) as timestamp,
- ${hasFunctions ? 'function_id,' : ''}
- avg(m.execution_time_ms) as avg_execution_time
-from
- function_edge_logs
- cross join unnest(metadata) as m
- cross join unnest(m.request) as request
- ${hasFunctions ? `where function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` : ''}
-group by
- timestamp
- ${hasFunctions ? ', function_id' : ''}
-order by
- timestamp desc
- `
- },
-}
-
-/**
- * FORMATTERS.
- * Metrics need to be formatted before being passed on to the UI charts.
- */
-
-function defaultFormatter(rawData: any, attributes: MultiAttribute[]) {
- const chartAttributes = attributes
- if (!rawData) return { data: undefined, chartAttributes }
- const result = rawData.result || []
- const timestamps = new Set
(result.map((p: any) => p.timestamp))
- const data = Array.from(timestamps)
- .sort()
- .map((timestamp) => {
- const point: any = { period_start: timestamp }
- chartAttributes.forEach((attr) => {
- point[attr.attribute] = 0
- })
- const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
- matchingPoints.forEach((p: any) => {
- point[attributes[0].attribute] = p.count
- })
- return point
- })
- return { data, chartAttributes }
-}
-
-const METRIC_FORMATTER: Record<
- MetricKey,
- (
- rawData: any,
- attributes: MultiAttribute[],
- logsMetric: string,
- functionIds?: string[],
- edgeFnIdToName?: (id: string) => string | undefined
- ) => { data: any; chartAttributes: any }
-> = {
- TotalInvocations: (rawData, attributes, logsMetric, functionIds, edgeFnIdToName) => {
- // Always use dynamic attributes, so the chart can show per-function stats.
- if (!rawData) return { data: undefined, chartAttributes: attributes }
- const result = rawData.result || []
-
- const functionIdsInData = Array.from(new Set(result.map((p: any) => p.function_id))) as string[]
-
- const chartFunctionIds = functionIds && functionIds.length > 0 ? functionIds : functionIdsInData
-
- if (chartFunctionIds.length === 0) {
- return { data: [], chartAttributes: [] } // No data, empty chart
- }
-
- const chartAttributes = chartFunctionIds.map((id: string) => ({
- attribute: id,
- label: edgeFnIdToName?.(id) ?? id,
- provider: 'logs',
- enabled: true,
- }))
-
- const timestamps = new Set(result.map((p: any) => p.timestamp))
- const data = Array.from(timestamps)
- .sort()
- .map((timestamp) => {
- const point: any = { period_start: timestamp }
- chartAttributes.forEach((attr) => {
- point[attr.attribute] = 0
- })
- const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
- matchingPoints.forEach((p: any) => {
- point[p.function_id as string] = p.count
- })
- return point
- })
-
- return { data, chartAttributes }
- },
- ExecutionStatusCodes: (rawData, attributes) => {
- if (!rawData) return { data: undefined, chartAttributes: attributes }
- const result = rawData.result || []
-
- const statusCodes = Array.from(new Set(result.map((p: any) => p.status_code)))
-
- const chartAttributes = statusCodes.map((statusCode) => {
- const statusCodeInfo = getHttpStatusCodeInfo(Number(statusCode))
- const color =
- REPORT_STATUS_CODE_COLORS[String(statusCode)] || REPORT_STATUS_CODE_COLORS.default
-
- return {
- attribute: `status_${statusCode}`,
- label: `${statusCode} ${statusCodeInfo.label}`,
- provider: 'logs',
- enabled: true,
- color: color,
- statusCode: String(statusCode),
- }
- })
-
- const timestamps = new Set(result.map((p: any) => p.timestamp))
- const data = Array.from(timestamps)
- .sort()
- .map((timestamp) => {
- const point: any = { period_start: timestamp }
- chartAttributes.forEach((attr) => {
- point[attr.attribute] = 0
- })
- const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
- matchingPoints.forEach((p: any) => {
- point[`status_${p.status_code}`] = p.count
- })
- return point
- })
-
- return { data, chartAttributes }
- },
- InvocationsByRegion: (rawData, attributes) => {
- if (!rawData) return { data: undefined, chartAttributes: attributes }
- const result = rawData.result || []
-
- const regions = Array.from(new Set(result.map((p: any) => p.region))).filter(Boolean)
-
- if (regions.length === 0) {
- return { data: [], chartAttributes: [] } // No data, empty chart
- }
-
- const chartAttributes = regions.map((region) => {
- return {
- attribute: region,
- label: region,
- provider: 'logs',
- enabled: true,
- }
- })
-
- const timestamps = new Set(result.map((p: any) => p.timestamp))
- const data = Array.from(timestamps)
- .sort()
- .map((timestamp) => {
- const point: any = { period_start: timestamp }
- chartAttributes.forEach((attr) => {
- point[attr.attribute as string] = 0
- })
- const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
- matchingPoints.forEach((p: any) => {
- point[p.region] = p.count
- })
- return point
- })
-
- return { data, chartAttributes }
- },
- ExecutionTime: (rawData, attributes, logsMetric, functionIds, edgeFnIdToName) => {
- if (!rawData) return { data: undefined, chartAttributes: attributes }
- const result = rawData.result || []
- const hasFunctions = functionIds && functionIds.length > 0
-
- if (hasFunctions) {
- const chartAttributes = functionIds.map((id: string) => ({
- attribute: id,
- label: edgeFnIdToName?.(id) ?? id,
- provider: 'logs',
- enabled: true,
- }))
-
- if (result.length === 0) {
- return { data: [], chartAttributes }
- }
-
- const timestamps = new Set(result.map((p: any) => p.timestamp))
- const data = Array.from(timestamps)
- .sort()
- .map((timestamp) => {
- const point: any = { period_start: timestamp }
- chartAttributes.forEach((attr) => {
- point[attr.attribute] = 0
- })
- const matchingPoints = result.filter((p: any) => p.timestamp === timestamp)
- matchingPoints.forEach((p: any) => {
- point[p.function_id as string] = p.avg_execution_time
- })
- return point
- })
-
- return { data, chartAttributes }
- } else {
- const chartAttributes = [
- {
- attribute: 'avg_execution_time',
- label: 'Avg. execution time (ms)',
- provider: 'logs',
- enabled: true,
- },
- ]
-
- const data = result
- .map((p: any) => ({
- period_start: p.timestamp,
- avg_execution_time: p.avg_execution_time,
- }))
- .sort(
- (a: { period_start: string }, b: { period_start: string }) =>
- new Date(a.period_start).getTime() - new Date(b.period_start).getTime()
- )
-
- return { data, chartAttributes }
- }
- },
-}
-
-/**
- * REPORT QUERY.
- * Fetching and state management for the report.
- */
-
-export function useEdgeFunctionReport({
- projectRef,
- attributes,
- startDate,
- endDate,
- interval,
- enabled = true,
- functionIds,
-}: {
- projectRef: string
- attributes: MultiAttribute[]
- startDate: string
- endDate: string
- interval: AnalyticsInterval
- enabled?: boolean
- functionIds?: string[]
-}) {
- const logsMetric = attributes.length > 0 ? attributes[0].attribute : ''
- const { edgeFnIdToName } = useEdgeFnIdToName({ projectRef })
- const isEdgeFnMetric = METRIC_KEYS.includes(logsMetric)
-
- const sql = isEdgeFnMetric ? METRIC_SQL[logsMetric as MetricKey](interval, functionIds) : ''
-
- const {
- data: rawData,
- error,
- isLoading,
- } = useQuery(
- [
- 'edge-function-report',
- projectRef,
- logsMetric,
- startDate,
- endDate,
- interval,
- sql,
- functionIds,
- ],
- async () => {
- const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
- params: {
- path: { ref: projectRef },
- query: {
- sql,
- iso_timestamp_start: startDate,
- iso_timestamp_end: endDate,
- },
- },
- })
- if (error) throw error
- return data
- },
- {
- enabled: Boolean(projectRef && sql && enabled && isEdgeFnMetric),
- refetchOnWindowFocus: false,
- }
- )
-
- // Use formatter if available
- const formatter =
- (isEdgeFnMetric ? METRIC_FORMATTER[logsMetric as MetricKey] : undefined) || defaultFormatter
- const { data, chartAttributes } = formatter(
- rawData,
- attributes,
- logsMetric,
- functionIds,
- edgeFnIdToName
- )
-
- return {
- data,
- attributes: chartAttributes,
- isLoading,
- error,
- }
-}
diff --git a/apps/studio/data/reports/v2/edge-functions.config.ts b/apps/studio/data/reports/v2/edge-functions.config.ts
new file mode 100644
index 00000000000..2e65c925232
--- /dev/null
+++ b/apps/studio/data/reports/v2/edge-functions.config.ts
@@ -0,0 +1,395 @@
+import dayjs from 'dayjs'
+
+import {
+ isUnixMicro,
+ unixMicroToIsoTimestamp,
+} from 'components/interfaces/Settings/Logs/Logs.utils'
+import type { AnalyticsInterval } from 'data/analytics/constants'
+import { get } from 'data/fetchers'
+import {
+ analyticsIntervalToGranularity,
+ REPORT_STATUS_CODE_COLORS,
+} from 'data/reports/report.utils'
+import { getHttpStatusCodeInfo } from 'lib/http-status-codes'
+import { ReportConfig } from './reports.types'
+
+const METRIC_SQL: Record string> =
+ {
+ TotalInvocations: (interval, functionIds) => {
+ return `
+--edgefn-report-invocations
+select
+ timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
+ function_id,
+ count(*) as count
+from
+ function_edge_logs
+ CROSS JOIN UNNEST(metadata) AS m
+ CROSS JOIN UNNEST(m.request) AS request
+ CROSS JOIN UNNEST(m.response) AS response
+ ${
+ functionIds && functionIds.length > 0
+ ? `WHERE function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
+ : ''
+ }
+group by
+ timestamp,
+ function_id
+order by
+ timestamp desc;
+`
+ },
+ ExecutionStatusCodes: (interval, functionIds) => {
+ return `
+--edgefn-report-execution-status-codes
+select
+ timestamp_trunc(timestamp, ${analyticsIntervalToGranularity(interval)}) as timestamp,
+ response.status_code as status_code,
+ count(response.status_code) as count
+from
+ function_edge_logs
+ cross join unnest(metadata) as m
+ cross join unnest(m.response) as response
+ ${
+ functionIds && functionIds.length > 0
+ ? `where function_id in (${functionIds.map((id) => `'${id}'`).join(',')})`
+ : ''
+ }
+group by
+ timestamp,
+ status_code
+order by
+ timestamp desc
+`
+ },
+ InvocationsByRegion: (interval, functionIds) => {
+ const granularity = analyticsIntervalToGranularity(interval)
+ return `
+--edgefn-report-invocations-by-region
+select
+ timestamp_trunc(timestamp, ${granularity}) as timestamp,
+ h.x_sb_edge_region as region,
+ count(*) as count
+from
+ function_edge_logs
+ cross join unnest(metadata) as m
+ cross join unnest(m.response) as r
+ cross join unnest(r.headers) as h
+ where h.x_sb_edge_region is not null
+ ${
+ functionIds && functionIds.length > 0
+ ? `and function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})`
+ : ''
+ }
+group by
+ timestamp,
+ region
+order by
+ timestamp desc
+`
+ },
+ ExecutionTime: (interval, functionIds) => {
+ const granularity = analyticsIntervalToGranularity(interval)
+ const hasFunctions = functionIds && functionIds.length > 0
+ return `
+--edgefn-report-execution-time
+select
+ timestamp_trunc(timestamp, ${granularity}) as timestamp,
+ ${hasFunctions ? 'function_id,' : ''}
+ avg(m.execution_time_ms) as avg_execution_time
+from
+ function_edge_logs
+ cross join unnest(metadata) as m
+ cross join unnest(m.request) as request
+ ${hasFunctions ? `where function_id IN (${functionIds.map((id) => `'${id}'`).join(',')})` : ''}
+group by
+ timestamp
+ ${hasFunctions ? ', function_id' : ''}
+order by
+ timestamp desc
+`
+ },
+ }
+
+async function runQuery(projectRef: string, sql: string, startDate: string, endDate: string) {
+ const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
+ params: {
+ path: { ref: projectRef },
+ query: {
+ sql,
+ iso_timestamp_start: startDate,
+ iso_timestamp_end: endDate,
+ },
+ },
+ })
+ if (error) throw error
+ return data
+}
+
+export function extractStatusCodesFromData(data: any[]): string[] {
+ const statusCodes = new Set()
+
+ data.forEach((item: any) => {
+ if (item.status_code) {
+ statusCodes.add(String(item.status_code))
+ }
+ })
+
+ return Array.from(statusCodes).sort()
+}
+
+export function generateStatusCodeAttributes(statusCodes: string[]) {
+ return statusCodes.map((code) => ({
+ attribute: code,
+ label: `${code} ${getHttpStatusCodeInfo(parseInt(code)).label}`,
+ color: REPORT_STATUS_CODE_COLORS[code] || REPORT_STATUS_CODE_COLORS.default,
+ }))
+}
+
+/**
+ * Converts a list of { timestamp, status_code, count }
+ * to a list of { timestamp, [status_code]: count }
+ * That we can pass to the chart for rendering
+ */
+export function transformStatusCodeData(data: any[], statusCodes: string[]) {
+ const pivotedData = data.reduce((acc: Record, d: any) => {
+ const timestamp = isUnixMicro(d.timestamp)
+ ? unixMicroToIsoTimestamp(d.timestamp)
+ : dayjs.utc(d.timestamp).toISOString()
+ if (!acc[timestamp]) {
+ acc[timestamp] = { timestamp }
+ statusCodes.forEach((code) => {
+ acc[timestamp][code] = 0
+ })
+ }
+ acc[timestamp][d.status_code] = d.count
+ return acc
+ }, {})
+
+ return Object.values(pivotedData)
+}
+
+/**
+ * Transforms raw invocation data by normalizing timestamps and adding function names
+ * @param data - Raw data from the database
+ * @param functions - Array of function objects with id and name
+ * @returns Transformed data with normalized timestamps and function names
+ */
+export function transformInvocationData(data: any[], functions: { id: string; name: string }[]) {
+ return data.map((log: any) => ({
+ ...log,
+ timestamp: isUnixMicro(log.timestamp)
+ ? unixMicroToIsoTimestamp(log.timestamp)
+ : dayjs.utc(log.timestamp).toISOString(),
+ function_name: functions.find((f) => f.id === log.function_id)?.name ?? log.function_id,
+ }))
+}
+
+/**
+ * Aggregates invocation data by timestamp, summing counts for each timestamp
+ * @param data - Transformed invocation data
+ * @returns Aggregated data with one entry per timestamp
+ */
+export function aggregateInvocationsByTimestamp(data: any[]) {
+ const aggregatedData = data.reduce((acc: Record, item: any) => {
+ const timestamp = item.timestamp
+ if (!acc[timestamp]) {
+ acc[timestamp] = { timestamp, count: 0 }
+ }
+ acc[timestamp].count += item.count
+ return acc
+ }, {})
+
+ return Object.values(aggregatedData)
+}
+
+export const edgeFunctionReports = ({
+ projectRef,
+ functions,
+ startDate,
+ endDate,
+ interval,
+ filters,
+}: {
+ projectRef: string
+ functions: { id: string; name: string }[]
+ startDate: string
+ endDate: string
+ interval: AnalyticsInterval
+ filters: {
+ functionIds?: string[]
+ }
+}): ReportConfig[] => [
+ {
+ id: 'total-invocations',
+ label: 'Total Edge Function Invocations',
+ valuePrecision: 0,
+ hide: false,
+ showTooltip: true,
+ showLegend: true,
+ showMaxValue: false,
+ hideChartType: false,
+ defaultChartStyle: 'line',
+ titleTooltip: 'The total number of edge function invocations over time.',
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
+ dataProvider: async () => {
+ const sql = METRIC_SQL.TotalInvocations(interval, filters.functionIds)
+ const response = await runQuery(projectRef, sql, startDate, endDate)
+
+ if (!response?.result) return { data: [] }
+
+ // Transform and aggregate the data using extracted functions
+ const transformedData = transformInvocationData(response.result, functions)
+ const data = aggregateInvocationsByTimestamp(transformedData)
+
+ const attributes = [
+ {
+ attribute: 'count',
+ label: 'Count',
+ },
+ ]
+
+ return { data, attributes, query: sql }
+ },
+ },
+ {
+ id: 'execution-status-codes',
+ label: 'Edge Function Status Codes',
+ valuePrecision: 0,
+ hide: false,
+ showTooltip: true,
+ showLegend: true,
+ showMaxValue: false,
+ hideChartType: false,
+ defaultChartStyle: 'line',
+ titleTooltip: 'The total number of edge function executions by status code.',
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
+ dataProvider: async (
+ projectRef: string,
+ startDate: string,
+ endDate: string,
+ interval: AnalyticsInterval,
+ functionIds?: string[]
+ ) => {
+ const sql = METRIC_SQL.ExecutionStatusCodes(interval, functionIds)
+ const rawData = await runQuery(projectRef, sql, startDate, endDate)
+
+ if (!rawData?.result) return { data: [] }
+
+ /**
+ * The query returns { timestamp, status_code: 500, count: 10 }
+ * and we have to transform it to { timestamp, 500: 10 }
+ * to be able to render the chart.
+ */
+
+ const statusCodes = extractStatusCodesFromData(rawData.result)
+ const attributes = generateStatusCodeAttributes(statusCodes)
+
+ const data = transformStatusCodeData(rawData.result, statusCodes)
+
+ return { data, attributes, query: sql }
+ },
+ },
+ {
+ id: 'execution-time',
+ label: 'Edge Function Execution Time',
+ valuePrecision: 0,
+ hide: false,
+ showTooltip: true,
+ showLegend: true,
+ showMaxValue: false,
+ hideChartType: false,
+ defaultChartStyle: 'line',
+ titleTooltip: 'Average execution time for edge functions.',
+ availableIn: ['free', 'pro', 'team', 'enterprise'],
+ YAxisProps: {
+ width: 50,
+ tickFormatter: (value: number) => `${value}ms`,
+ },
+ format: (value: unknown) => `${Number(value).toFixed(0)}ms`,
+ dataProvider: async () => {
+ const sql = METRIC_SQL.ExecutionTime(interval, filters.functionIds)
+ const rawData = await runQuery(projectRef, sql, startDate, endDate)
+
+ if (!rawData?.result) return { data: [] }
+
+ // Transform the raw data to ensure one data point per timestamp
+ const transformedData = rawData.result?.map((point: any) => ({
+ ...point,
+ timestamp: isUnixMicro(point.timestamp)
+ ? unixMicroToIsoTimestamp(point.timestamp)
+ : dayjs.utc(point.timestamp).toISOString(),
+ function_name: functions.find((f) => f.id === point.function_id)?.name ?? point.function_id,
+ }))
+
+ // If we have multiple function IDs, we need to aggregate the execution times per timestamp
+ const aggregatedData = transformedData.reduce((acc: Record, item: any) => {
+ const timestamp = item.timestamp
+ if (!acc[timestamp]) {
+ acc[timestamp] = {
+ timestamp,
+ avg_execution_time: item.avg_execution_time,
+ count: 1,
+ }
+ } else {
+ // Calculate weighted average for multiple functions at the same timestamp
+ const totalTime =
+ acc[timestamp].avg_execution_time * acc[timestamp].count + item.avg_execution_time
+ acc[timestamp].count += 1
+ acc[timestamp].avg_execution_time = totalTime / acc[timestamp].count
+ }
+ return acc
+ }, {})
+
+ const data = Object.values(aggregatedData).map(({ count, ...item }) => item)
+
+ const attributes = [
+ {
+ attribute: 'avg_execution_time',
+ label: 'Avg. execution time (ms)',
+ },
+ ]
+ return { data, attributes, query: sql }
+ },
+ },
+ {
+ id: 'invocations-by-region',
+ label: 'Edge Function Invocations by Region',
+ valuePrecision: 0,
+ hide: false,
+ showTooltip: true,
+ showLegend: true,
+ showMaxValue: false,
+ hideChartType: false,
+ defaultChartStyle: 'line',
+ titleTooltip: 'The total number of edge function invocations by region.',
+ availableIn: ['pro', 'team', 'enterprise'],
+ dataProvider: async () => {
+ const sql = METRIC_SQL.InvocationsByRegion(interval, filters.functionIds)
+ const rawData = await runQuery(projectRef, sql, startDate, endDate)
+ const data = rawData.result?.map((point: any) => ({
+ ...point,
+ timestamp: isUnixMicro(point.timestamp)
+ ? unixMicroToIsoTimestamp(point.timestamp)
+ : dayjs.utc(point.timestamp).toISOString(),
+ }))
+
+ const attributes = [
+ {
+ attribute: 'region',
+ label: 'Region',
+ provider: 'logs',
+ enabled: true,
+ },
+ {
+ attribute: 'count',
+ label: 'Count',
+ provider: 'logs',
+ enabled: true,
+ },
+ ]
+
+ return { data, attributes, query: sql }
+ },
+ },
+]
diff --git a/apps/studio/data/reports/v2/edge-functions.test.tsx b/apps/studio/data/reports/v2/edge-functions.test.tsx
new file mode 100644
index 00000000000..e1217d7b7a0
--- /dev/null
+++ b/apps/studio/data/reports/v2/edge-functions.test.tsx
@@ -0,0 +1,251 @@
+import { describe, expect, it } from 'vitest'
+import {
+ extractStatusCodesFromData,
+ generateStatusCodeAttributes,
+ transformStatusCodeData,
+ transformInvocationData,
+ aggregateInvocationsByTimestamp,
+} from './edge-functions.config'
+
+describe('extractStatusCodesFromData', () => {
+ it('should extract and sort unique status codes from the data', () => {
+ const data = [
+ { status_code: 200 },
+ { status_code: 500 },
+ { status_code: 200 },
+ { status_code: 404 },
+ ]
+ const result = extractStatusCodesFromData(data)
+ expect(result).toEqual(['200', '404', '500'])
+ })
+
+ it('should handle an empty array', () => {
+ const data: any[] = []
+ const result = extractStatusCodesFromData(data)
+ expect(result).toEqual([])
+ })
+
+ it('should handle data with missing status_code properties', () => {
+ const data = [{ status_code: 200 }, {}, { status_code: 500 }]
+ const result = extractStatusCodesFromData(data)
+ expect(result).toEqual(['200', '500'])
+ })
+
+ it('should handle various data types for status_code', () => {
+ const data = [{ status_code: 200 }, { status_code: '500' }, { status_code: 404 }]
+ const result = extractStatusCodesFromData(data)
+ expect(result).toEqual(['200', '404', '500'])
+ })
+})
+
+describe('generateStatusCodeAttributes', () => {
+ it('should generate the correct attributes for a list of status codes', () => {
+ const statusCodes = ['200', '404', '500']
+ const result = generateStatusCodeAttributes(statusCodes)
+ expect(result.map(({ color, ...rest }) => rest)).toEqual([
+ {
+ attribute: '200',
+ label: '200 OK',
+ },
+ {
+ attribute: '404',
+ label: '404 Not Found',
+ },
+ {
+ attribute: '500',
+ label: '500 Internal Server Error',
+ },
+ ])
+ })
+
+ it('should handle an empty array', () => {
+ const statusCodes: string[] = []
+ const result = generateStatusCodeAttributes(statusCodes)
+ expect(result).toEqual([])
+ })
+})
+
+describe('transformStatusCodeData', () => {
+ it('should pivot the data correctly', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00Z', status_code: 200, count: 10 },
+ { timestamp: '2023-01-01T00:00:00Z', status_code: 500, count: 5 },
+ { timestamp: '2023-01-02T00:00:00Z', status_code: 200, count: 20 },
+ ]
+ const result = transformStatusCodeData(data, ['200', '500'])
+ expect(result).toEqual([
+ { timestamp: '2023-01-01T00:00:00.000Z', '200': 10, '500': 5 },
+ { timestamp: '2023-01-02T00:00:00.000Z', '200': 20, '500': 0 },
+ ])
+ })
+
+ it('should handle an empty array', () => {
+ const data: any[] = []
+ const result = transformStatusCodeData(data, [])
+ expect(result).toEqual([])
+ })
+
+ it('should handle a single entry', () => {
+ const data = [{ timestamp: '2023-01-01T00:00:00Z', status_code: 200, count: 10 }]
+ const result = transformStatusCodeData(data, ['200', '404'])
+ expect(result).toEqual([{ timestamp: '2023-01-01T00:00:00.000Z', '200': 10, '404': 0 }])
+ })
+})
+
+describe('transformInvocationData', () => {
+ const mockFunctions = [
+ { id: 'func1', name: 'Function One' },
+ { id: 'func2', name: 'Function Two' },
+ ]
+
+ it('should transform raw invocation data with function names', () => {
+ const rawData = [
+ {
+ timestamp: '2023-01-01T00:00:00Z',
+ function_id: 'func1',
+ count: 10,
+ },
+ {
+ timestamp: '2023-01-01T01:00:00Z',
+ function_id: 'func2',
+ count: 5,
+ },
+ ]
+
+ const result = transformInvocationData(rawData, mockFunctions)
+ expect(result).toEqual([
+ {
+ timestamp: '2023-01-01T00:00:00.000Z',
+ function_id: 'func1',
+ count: 10,
+ function_name: 'Function One',
+ },
+ {
+ timestamp: '2023-01-01T01:00:00.000Z',
+ function_id: 'func2',
+ count: 5,
+ function_name: 'Function Two',
+ },
+ ])
+ })
+
+ it('should use function_id as fallback when function name not found', () => {
+ const rawData = [
+ {
+ timestamp: '2023-01-01T00:00:00Z',
+ function_id: 'unknown_func',
+ count: 10,
+ },
+ ]
+
+ const result = transformInvocationData(rawData, mockFunctions)
+ expect(result).toEqual([
+ {
+ timestamp: '2023-01-01T00:00:00.000Z',
+ function_id: 'unknown_func',
+ count: 10,
+ function_name: 'unknown_func',
+ },
+ ])
+ })
+
+ it('should handle unix micro timestamps', () => {
+ const rawData = [
+ {
+ timestamp: 1672531200000000, // Unix micro timestamp for 2023-01-01T00:00:00Z
+ function_id: 'func1',
+ count: 10,
+ },
+ ]
+
+ const result = transformInvocationData(rawData, mockFunctions)
+ expect(result[0].timestamp).toBe('2023-01-01T00:00:00.000Z')
+ expect(result[0].function_name).toBe('Function One')
+ })
+
+ it('should handle empty data array', () => {
+ const result = transformInvocationData([], mockFunctions)
+ expect(result).toEqual([])
+ })
+
+ it('should handle empty functions array', () => {
+ const rawData = [
+ {
+ timestamp: '2023-01-01T00:00:00Z',
+ function_id: 'func1',
+ count: 10,
+ },
+ ]
+
+ const result = transformInvocationData(rawData, [])
+ expect(result).toEqual([
+ {
+ timestamp: '2023-01-01T00:00:00.000Z',
+ function_id: 'func1',
+ count: 10,
+ function_name: 'func1',
+ },
+ ])
+ })
+})
+
+describe('aggregateInvocationsByTimestamp', () => {
+ it('should aggregate counts by timestamp', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func1', count: 10 },
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func2', count: 5 },
+ { timestamp: '2023-01-01T01:00:00.000Z', function_id: 'func1', count: 20 },
+ ]
+
+ const result = aggregateInvocationsByTimestamp(data)
+ expect(result).toEqual([
+ { timestamp: '2023-01-01T00:00:00.000Z', count: 15 },
+ { timestamp: '2023-01-01T01:00:00.000Z', count: 20 },
+ ])
+ })
+
+ it('should handle single entry per timestamp', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func1', count: 10 },
+ { timestamp: '2023-01-01T01:00:00.000Z', function_id: 'func2', count: 5 },
+ ]
+
+ const result = aggregateInvocationsByTimestamp(data)
+ expect(result).toEqual([
+ { timestamp: '2023-01-01T00:00:00.000Z', count: 10 },
+ { timestamp: '2023-01-01T01:00:00.000Z', count: 5 },
+ ])
+ })
+
+ it('should handle empty data array', () => {
+ const result = aggregateInvocationsByTimestamp([])
+ expect(result).toEqual([])
+ })
+
+ it('should handle multiple entries with same timestamp and different counts', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func1', count: 1 },
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func2', count: 2 },
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func3', count: 3 },
+ ]
+
+ const result = aggregateInvocationsByTimestamp(data)
+ expect(result).toEqual([{ timestamp: '2023-01-01T00:00:00.000Z', count: 6 }])
+ })
+
+ it('should preserve timestamp order from reduce operation', () => {
+ const data = [
+ { timestamp: '2023-01-01T02:00:00.000Z', function_id: 'func1', count: 30 },
+ { timestamp: '2023-01-01T00:00:00.000Z', function_id: 'func1', count: 10 },
+ { timestamp: '2023-01-01T01:00:00.000Z', function_id: 'func1', count: 20 },
+ ]
+
+ const result = aggregateInvocationsByTimestamp(data)
+ expect(result).toHaveLength(3)
+ expect(result.map((item) => item.timestamp)).toEqual([
+ '2023-01-01T02:00:00.000Z',
+ '2023-01-01T00:00:00.000Z',
+ '2023-01-01T01:00:00.000Z',
+ ])
+ })
+})
diff --git a/apps/studio/data/reports/v2/reports.types.ts b/apps/studio/data/reports/v2/reports.types.ts
new file mode 100644
index 00000000000..5edd0b23791
--- /dev/null
+++ b/apps/studio/data/reports/v2/reports.types.ts
@@ -0,0 +1,45 @@
+import { AnalyticsInterval } from 'data/analytics/constants'
+import { YAxisProps } from 'recharts'
+
+type ReportDataProviderFilter = {
+ functionIds?: string[]
+}
+
+export interface ReportDataProvider {
+ (
+ projectRef: string,
+ startDate: string,
+ endDate: string,
+ interval: AnalyticsInterval,
+ functionIds?: string[],
+ edgeFnIdToName?: (id: string) => string | undefined,
+ filters?: ReportDataProviderFilter[]
+ ): Promise<{
+ data: any
+ attributes?: {
+ attribute: string
+ label: string
+ color?: { light: string; dark: string }
+ }[]
+ query?: string // The SQL used to fetch the data if any
+ }> // [jordi] would be cool to have a type that forces data keys to match the attributes
+}
+
+export interface ReportConfig {
+ id: string
+ label: string
+ dataProvider: ReportDataProvider
+ valuePrecision: number
+ hide: boolean
+ showTooltip: boolean
+ showLegend: boolean
+ showMaxValue: boolean
+ hideChartType: boolean
+ defaultChartStyle: string
+ titleTooltip: string
+ availableIn: string[]
+ format?: (value: unknown) => string
+ YAxisProps?: YAxisProps
+ xAxisKey?: string
+ yAxisKey?: string
+}
diff --git a/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts b/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts
index 4319a90f7cb..7f8629b974d 100644
--- a/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts
+++ b/apps/studio/hooks/branches/useEdgeFunctionsDiff.ts
@@ -201,13 +201,15 @@ export const useEdgeFunctionsDiff = ({
const mainBody = mainBodiesMap[slug]
if (!currentBody || !mainBody) return
- const allFileKeys = new Set([...currentBody, ...mainBody].map((f) => fileKey(f.name)))
+ const allFileKeys = new Set(
+ [...currentBody.files, ...mainBody.files].map((f) => fileKey(f.name))
+ )
const fileInfos: FileInfo[] = []
let hasModifications = false
for (const key of allFileKeys) {
- const currentFile = currentBody.find((f) => fileKey(f.name) === key)
- const mainFile = mainBody.find((f) => fileKey(f.name) === key)
+ const currentFile = currentBody.files.find((f) => fileKey(f.name) === key)
+ const mainFile = mainBody.files.find((f) => fileKey(f.name) === key)
let status: FileStatus = 'unchanged'
@@ -236,7 +238,7 @@ export const useEdgeFunctionsDiff = ({
addedSlugs.forEach((slug) => {
const body = addedBodiesMap[slug]
if (body) {
- functionFileInfo[slug] = body.map((file) => ({
+ functionFileInfo[slug] = body.files.map((file) => ({
key: fileKey(file.name),
status: 'added' as FileStatus,
}))
@@ -247,7 +249,7 @@ export const useEdgeFunctionsDiff = ({
removedSlugs.forEach((slug) => {
const body = removedBodiesMap[slug]
if (body) {
- functionFileInfo[slug] = body.map((file) => ({
+ functionFileInfo[slug] = body.files.map((file) => ({
key: fileKey(file.name),
status: 'removed' as FileStatus,
}))
diff --git a/apps/studio/hooks/custom-content/CustomContent.types.ts b/apps/studio/hooks/custom-content/CustomContent.types.ts
index 66816dffa96..28568d054b3 100644
--- a/apps/studio/hooks/custom-content/CustomContent.types.ts
+++ b/apps/studio/hooks/custom-content/CustomContent.types.ts
@@ -1,4 +1,5 @@
-import { ConnectionType } from 'components/interfaces/Connect/Connect.constants'
+import { CONNECTION_TYPES } from 'components/interfaces/Connect/Connect.constants'
+import type { CloudProvider } from 'shared-data'
export type CustomContentTypes = {
organizationLegalDocuments: {
@@ -25,9 +26,7 @@ export type CustomContentTypes = {
*
* These could be helpful in rendering, for e.g an environment file like `.env`
*/
- connectFrameworks: {
- key: string
- label: string
- obj: ConnectionType[]
- }
+ connectFrameworks: (typeof CONNECTION_TYPES)[number]
+
+ infraCloudProviders: CloudProvider[]
}
diff --git a/apps/studio/hooks/custom-content/custom-content.json b/apps/studio/hooks/custom-content/custom-content.json
index 1378a1822a9..57bd6db86b7 100644
--- a/apps/studio/hooks/custom-content/custom-content.json
+++ b/apps/studio/hooks/custom-content/custom-content.json
@@ -7,5 +7,7 @@
"logs:default_query": null,
- "connect:frameworks": null
+ "connect:frameworks": null,
+
+ "infra:cloud_providers": ["AWS", "AWS_K8S", "FLY"]
}
diff --git a/apps/studio/hooks/custom-content/custom-content.sample.json b/apps/studio/hooks/custom-content/custom-content.sample.json
index 7992201514d..da5a880f342 100644
--- a/apps/studio/hooks/custom-content/custom-content.sample.json
+++ b/apps/studio/hooks/custom-content/custom-content.sample.json
@@ -72,5 +72,7 @@
]
}
]
- }
+ },
+
+ "infra:cloud_providers": ["AWS_NIMBUS"]
}
diff --git a/apps/studio/hooks/custom-content/custom-content.schema.json b/apps/studio/hooks/custom-content/custom-content.schema.json
index 6d7020a3447..adea1a5f9fe 100644
--- a/apps/studio/hooks/custom-content/custom-content.schema.json
+++ b/apps/studio/hooks/custom-content/custom-content.schema.json
@@ -72,13 +72,23 @@
}
}
}
+ },
+
+ "infra:cloud_providers": {
+ "type": ["array"],
+ "description": "Filters the valid cloud providers",
+ "items": {
+ "type": "string",
+ "enum": ["AWS", "AWS_K8S", "AWS_NIMBUS", "FLY"]
+ }
}
},
"required": [
"organization:legal_documents",
"project_homepage:example_projects",
"logs:default_query",
- "connect:frameworks"
+ "connect:frameworks",
+ "infra:cloud_providers"
],
"additionalProperties": false
}
diff --git a/apps/studio/hooks/custom-content/useCustomContent.ts b/apps/studio/hooks/custom-content/useCustomContent.ts
index 0d1b2342642..8f26fe1f397 100644
--- a/apps/studio/hooks/custom-content/useCustomContent.ts
+++ b/apps/studio/hooks/custom-content/useCustomContent.ts
@@ -28,16 +28,16 @@ const useCustomContent = (
contents: T
): {
[key in CustomContentToCamelCase]:
- | (typeof customContentStaticObj)[CustomContent]
| CustomContentTypes[CustomContentToCamelCase]
+ | null
} => {
// [Joshen] Running into some TS errors without the `as` here - must be overlooking something super simple
return Object.fromEntries(
contents.map((content) => [contentToCamelCase(content), customContentStaticObj[content]])
) as {
[key in CustomContentToCamelCase]:
- | (typeof customContentStaticObj)[CustomContent]
| CustomContentTypes[CustomContentToCamelCase]
+ | null
}
}
diff --git a/apps/studio/hooks/misc/useReportDateRange.ts b/apps/studio/hooks/misc/useReportDateRange.ts
index af9502de124..bd1be26821c 100644
--- a/apps/studio/hooks/misc/useReportDateRange.ts
+++ b/apps/studio/hooks/misc/useReportDateRange.ts
@@ -9,6 +9,7 @@ import {
} from 'components/interfaces/Reports/Reports.constants'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { maybeShowUpgradePrompt } from 'components/interfaces/Settings/Logs/Logs.utils'
+import { AnalyticsInterval } from 'data/analytics/constants'
export const DATERANGE_LIMITS: { [key: string]: number } = {
free: 1,
@@ -20,7 +21,7 @@ export const DATERANGE_LIMITS: { [key: string]: number } = {
export interface ReportDateRange {
period_start: { date: string; time_period: string }
period_end: { date: string; time_period: string }
- interval: string
+ interval: AnalyticsInterval
}
// Create parsers for individual URL parameters
diff --git a/apps/studio/hooks/useChartData.ts b/apps/studio/hooks/useChartData.ts
index 63998abc939..29f129b464a 100644
--- a/apps/studio/hooks/useChartData.ts
+++ b/apps/studio/hooks/useChartData.ts
@@ -15,7 +15,6 @@ import type { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
import { useAuthLogsReport } from 'data/reports/auth-report-query'
import type { ChartData } from 'components/ui/Charts/Charts.types'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
-import { useEdgeFunctionReport } from 'data/reports/edgefn-query'
export const useChartData = ({
attributes,
@@ -56,25 +55,9 @@ export const useChartData = ({
enabled: enabled && logsAttributes.length > 0 && !isEdgeFunctionRoute,
})
- const {
- data: edgeFunctionData,
- attributes: edgeFunctionChartAttributes,
- isLoading: isEdgeFunctionLoading,
- } = useEdgeFunctionReport({
- projectRef: ref as string,
- attributes: logsAttributes,
- startDate,
- endDate,
- interval: interval as AnalyticsInterval,
- enabled: enabled && logsAttributes.length > 0 && isEdgeFunctionRoute,
- functionIds,
- })
-
- const logsData = isEdgeFunctionRoute ? edgeFunctionData : authData
- const logsChartAttributes = isEdgeFunctionRoute
- ? edgeFunctionChartAttributes
- : authChartAttributes
- const isLogsLoading = isEdgeFunctionRoute ? isEdgeFunctionLoading : isAuthLoading
+ const logsData = authData
+ const logsChartAttributes = authChartAttributes
+ const isLogsLoading = isAuthLoading
const combinedData = useMemo(() => {
if (data) return data
diff --git a/apps/studio/instrumentation-client.ts b/apps/studio/instrumentation-client.ts
index e1543a0e150..bafe2feee59 100644
--- a/apps/studio/instrumentation-client.ts
+++ b/apps/studio/instrumentation-client.ts
@@ -64,7 +64,7 @@ Sentry.init({
`Failed to construct 'URL': Invalid URL`
)
// [Joshen] Similar behaviour for this error from SessionTimeoutModal to control the quota usage
- const isSessionTimeoutEvent = (hint.originalException as any)?.message.includes(
+ const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes(
'Session error detected'
)
diff --git a/apps/studio/lib/constants/infrastructure.ts b/apps/studio/lib/constants/infrastructure.ts
index 4d2d4ffbf66..d9343193b46 100644
--- a/apps/studio/lib/constants/infrastructure.ts
+++ b/apps/studio/lib/constants/infrastructure.ts
@@ -2,6 +2,7 @@ import type { CloudProvider } from 'shared-data'
import { AWS_REGIONS, FLY_REGIONS } from 'shared-data'
import type { components } from 'data/api'
+import { useCustomContent } from 'hooks/custom-content/useCustomContent'
export const AWS_REGIONS_DEFAULT =
process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
@@ -33,11 +34,21 @@ export const PRICING_TIER_PRODUCT_IDS = {
ENTERPRISE: 'tier_enterprise',
}
-export const DEFAULT_PROVIDER: CloudProvider =
- process.env.NEXT_PUBLIC_ENVIRONMENT &&
- ['staging', 'preview'].includes(process.env.NEXT_PUBLIC_ENVIRONMENT)
- ? 'AWS_K8S'
- : 'AWS'
+export function useDefaultProvider() {
+ const defaultProvider: CloudProvider =
+ process.env.NEXT_PUBLIC_ENVIRONMENT &&
+ ['staging', 'preview'].includes(process.env.NEXT_PUBLIC_ENVIRONMENT)
+ ? 'AWS_K8S'
+ : 'AWS'
+
+ const { infraCloudProviders: validCloudProviders } = useCustomContent(['infra:cloud_providers'])
+
+ if (validCloudProviders?.includes(defaultProvider)) {
+ return defaultProvider
+ }
+
+ return (validCloudProviders?.[0] ?? 'AWS') as CloudProvider
+}
export const PROVIDERS = {
FLY: {
@@ -60,6 +71,12 @@ export const PROVIDERS = {
default_region: AWS_REGIONS_DEFAULT,
regions: { ...AWS_REGIONS },
},
+ AWS_NIMBUS: {
+ id: 'AWS_NIMBUS',
+ name: 'AWS (Nimbus)',
+ default_region: AWS_REGIONS_DEFAULT,
+ regions: { ...AWS_REGIONS },
+ },
} as const
export const PROJECT_STATUS: {
diff --git a/apps/studio/lib/eszip-parser.test.ts b/apps/studio/lib/eszip-parser.test.ts
index 4577549e85c..ec38531a19d 100644
--- a/apps/studio/lib/eszip-parser.test.ts
+++ b/apps/studio/lib/eszip-parser.test.ts
@@ -62,6 +62,7 @@ describe('eszip-parser', () => {
mockParser.parseBytes.mockResolvedValue(mockSpecifiers)
mockParser.load.mockResolvedValue(undefined)
mockParser.getModuleSource
+ .mockResolvedValueOnce(null)
.mockResolvedValueOnce(mockModuleSource1)
.mockResolvedValueOnce(mockModuleSource2)
@@ -70,12 +71,13 @@ describe('eszip-parser', () => {
expect(Parser.createInstance).toHaveBeenCalledTimes(1)
expect(mockParser.parseBytes).toHaveBeenCalledWith(mockBytes)
expect(mockParser.load).toHaveBeenCalled()
- expect(result).toHaveLength(2)
- expect(result[0]).toEqual({
+ expect(result.version).toEqual(0)
+ expect(result.files).toHaveLength(2)
+ expect(result.files[0]).toEqual({
name: 'file1.ts',
content: mockModuleSource1,
})
- expect(result[1]).toEqual({
+ expect(result.files[1]).toEqual({
name: 'file2.ts',
content: mockModuleSource2,
})
@@ -111,9 +113,10 @@ describe('eszip-parser', () => {
const result = await parseEszip(mockBytes)
// Only file1.ts and file2.ts should be included
- expect(result).toHaveLength(2)
- expect(result[0].name).toBe('file1.ts')
- expect(result[1].name).toBe('file2.ts')
+ expect(result.version).toEqual(0)
+ expect(result.files).toHaveLength(2)
+ expect(result.files[0].name).toBe('file1.ts')
+ expect(result.files[1].name).toBe('file2.ts')
})
})
})
diff --git a/apps/studio/lib/eszip-parser.ts b/apps/studio/lib/eszip-parser.ts
index bf84bc38c6a..7aa5428e747 100644
--- a/apps/studio/lib/eszip-parser.ts
+++ b/apps/studio/lib/eszip-parser.ts
@@ -63,8 +63,14 @@ export async function parseEszip(bytes: Uint8Array) {
throw loadError
}
+ // Extract version
+ let version = parseInt(await parser.getModuleSource('---SUPABASE-ESZIP-VERSION-ESZIP---'))
+ if (isNaN(version)) {
+ version = 0
+ }
+
// Extract files from the eszip
- const files = await extractEszip(parser, specifiers)
+ const files = await extractEszip(parser, specifiers, version >= 2)
// Convert files to the expected format
const responseFiles = await Promise.all(
@@ -77,14 +83,17 @@ export async function parseEszip(bytes: Uint8Array) {
})
)
- return responseFiles
+ return {
+ version,
+ files: responseFiles,
+ }
} catch (error) {
console.error('Error in parseEszip:', error)
throw error
}
}
-async function extractEszip(parser: any, specifiers: string[]) {
+async function extractEszip(parser: any, specifiers: string[], isDeno2: boolean) {
const files = []
// First, filter out the specifiers we want to keep
@@ -114,9 +123,13 @@ async function extractEszip(parser: any, specifiers: string[]) {
try {
// Try to get the module source
const moduleSource = await parser.getModuleSource(specifier)
+ let qualifiedSpecifier = specifier
// Get the file path
- const filePath = url2path(specifier)
+ if (isDeno2 && !specifier.startsWith('file://')) {
+ qualifiedSpecifier = `file://${specifier}`
+ }
+ const filePath = url2path(qualifiedSpecifier)
// Create a file object
const file = new File([moduleSource], filePath)
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index 19e0af38490..34b2be5f2d1 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -30,12 +30,12 @@ import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import Head from 'next/head'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'
-import { ErrorInfo } from 'react'
+import { ErrorInfo, useCallback } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import {
FeatureFlagProvider,
- getFlags as getConfigCatFlags,
+ getFlags,
TelemetryTagManager,
ThemeProvider,
useThemeSandbox,
@@ -51,7 +51,7 @@ import { GlobalErrorBoundaryState } from 'components/ui/GlobalErrorBoundaryState
import { useRootQueryClient } from 'data/query-client'
import { customFont, sourceCodePro } from 'fonts'
import { AuthProvider } from 'lib/auth'
-import { API_URL, BASE_PATH, IS_PLATFORM } from 'lib/constants'
+import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from 'lib/constants'
import { ProfileProvider } from 'lib/profile'
import { Telemetry } from 'lib/telemetry'
import { AppPropsWithLayout } from 'types'
@@ -100,6 +100,16 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test'
+ const cloudProvider = useDefaultProvider()
+ const getConfigCatFlags = useCallback(
+ (userEmail?: string) => {
+ const customAttributes = cloudProvider ? { cloud_provider: cloudProvider } : undefined
+
+ return getFlags(userEmail, customAttributes)
+ },
+ [cloudProvider]
+ )
+
return (
diff --git a/apps/studio/pages/api/edge-functions/body.ts b/apps/studio/pages/api/edge-functions/body.ts
index 317bddc189f..ed220a07144 100644
--- a/apps/studio/pages/api/edge-functions/body.ts
+++ b/apps/studio/pages/api/edge-functions/body.ts
@@ -81,11 +81,9 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const uint8Array = new Uint8Array(arrayBuffer)
// Parse the eszip file using our utility
- const files = await parseEszip(uint8Array)
+ const parsed = await parseEszip(uint8Array)
- return res.status(200).json({
- files,
- })
+ return res.status(200).json(parsed)
} catch (error) {
console.error('Error processing edge function body:', error)
return res.status(500).json({ error: 'Internal server error' })
diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx
index 51a50a4b11d..52e49b7aa04 100644
--- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx
+++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx
@@ -224,7 +224,7 @@ const CreateProject = () => {
{
const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
+ const { infraCloudProviders: validCloudProviders } = useCustomContent(['infra:cloud_providers'])
+
// This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org
if (slug === 'last-visited-org') {
if (lastVisitedOrganization) {
@@ -221,9 +224,11 @@ const Wizard: NextPageWithLayout = () => {
project.organization_id === currentOrg?.id && project.status !== PROJECT_STATUS.INACTIVE
) ?? []
+ const defaultProvider = useDefaultProvider()
+
const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
- cloudProvider: PROVIDERS[DEFAULT_PROVIDER].id,
+ cloudProvider: PROVIDERS[defaultProvider].id,
},
{
enabled: !smartRegionEnabled,
@@ -239,7 +244,7 @@ const Wizard: NextPageWithLayout = () => {
useOrganizationAvailableRegionsQuery(
{
slug: slug,
- cloudProvider: PROVIDERS[DEFAULT_PROVIDER].id,
+ cloudProvider: PROVIDERS[defaultProvider].id,
},
{
enabled: smartRegionEnabled,
@@ -304,7 +309,7 @@ const Wizard: NextPageWithLayout = () => {
organization: slug,
projectName: projectName || '',
postgresVersion: '',
- cloudProvider: PROVIDERS[DEFAULT_PROVIDER].id,
+ cloudProvider: PROVIDERS[defaultProvider].id,
dbPass: '',
dbPassStrength: 0,
dbRegion: defaultRegion || undefined,
@@ -455,7 +460,7 @@ const Wizard: NextPageWithLayout = () => {
useEffect(() => {
if (regionError) {
- form.setValue('dbRegion', PROVIDERS[DEFAULT_PROVIDER].default_region.displayName)
+ form.setValue('dbRegion', PROVIDERS[defaultProvider].default_region.displayName)
}
}, [regionError])
@@ -489,7 +494,7 @@ const Wizard: NextPageWithLayout = () => {
canCreateProject &&
additionalMonthlySpend > 0 && (
-
Additional Costs
+
Additional costs
${additionalMonthlySpend}/m
@@ -707,15 +712,20 @@ const Wizard: NextPageWithLayout = () => {
- {Object.values(PROVIDERS).map((providerObj) => {
- const label = providerObj['name']
- const value = providerObj['id']
- return (
-
- {label}
-
+ {Object.values(PROVIDERS)
+ .filter(
+ (provider) =>
+ validCloudProviders?.includes(provider.id) ?? true
)
- })}
+ .map((providerObj) => {
+ const label = providerObj['name']
+ const value = providerObj['id']
+ return (
+
+ {label}
+
+ )
+ })}
@@ -735,7 +745,7 @@ const Wizard: NextPageWithLayout = () => {
layout="horizontal"
label={
-
Compute Size
+
Compute size
{
href="https://supabase.com/docs/guides/platform/compute-add-ons"
>
-
Compute Add-Ons
+
Compute add-ons
@@ -754,7 +764,7 @@ const Wizard: NextPageWithLayout = () => {
href="https://supabase.com/docs/guides/platform/manage-your-usage/compute"
>
-
Compute Billing
+
Compute billing
@@ -845,7 +855,7 @@ const Wizard: NextPageWithLayout = () => {
return (
@@ -926,7 +936,7 @@ const Wizard: NextPageWithLayout = () => {
{
}
return (
-
+
{
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const {
- data: functionFiles,
+ data: functionBody,
isLoading: isLoadingFiles,
isError: isErrorLoadingFiles,
isSuccess: isSuccessLoadingFiles,
@@ -123,15 +123,29 @@ const CodePage = () => {
}
}
- function getBasePath(entrypoint: string | undefined): string {
+ function getBasePath(
+ entrypoint: string | undefined,
+ fileNames: string[],
+ version: number
+ ): string {
if (!entrypoint) {
return '/'
}
+ let qualifiedEntrypoint = entrypoint
+
+ if (version >= 2) {
+ const candidate = fileNames.find((name) => entrypoint.endsWith(name))
+ if (candidate) {
+ qualifiedEntrypoint = `file://${candidate}`
+ } else {
+ qualifiedEntrypoint = entrypoint
+ }
+ }
try {
- return dirname(new URL(entrypoint).pathname)
+ return dirname(new URL(qualifiedEntrypoint).pathname)
} catch (e) {
- console.error('Failed to parse entrypoint', entrypoint)
+ console.error('Failed to parse entrypoint', qualifiedEntrypoint)
return '/'
}
}
@@ -155,9 +169,13 @@ const CodePage = () => {
useEffect(() => {
// Set files from API response when available
- if (selectedFunction?.entrypoint_path && functionFiles) {
- const base_path = getBasePath(selectedFunction?.entrypoint_path)
- const filesWithRelPath = functionFiles
+ if (selectedFunction?.entrypoint_path && functionBody) {
+ const base_path = getBasePath(
+ selectedFunction?.entrypoint_path,
+ functionBody.files.map((file) => file.name),
+ functionBody.version
+ )
+ const filesWithRelPath = functionBody.files
// ignore empty files
.filter((file: { name: string; content: string }) => !!file.content.length)
// set file paths relative to entrypoint
@@ -192,7 +210,7 @@ const CodePage = () => {
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [functionFiles])
+ }, [functionBody])
return (
diff --git a/apps/studio/pages/project/[ref]/reports/auth.tsx b/apps/studio/pages/project/[ref]/reports/auth.tsx
index 3c65f9ca8f2..34838356c35 100644
--- a/apps/studio/pages/project/[ref]/reports/auth.tsx
+++ b/apps/studio/pages/project/[ref]/reports/auth.tsx
@@ -4,7 +4,7 @@ import dayjs from 'dayjs'
import { ArrowRight, RefreshCw } from 'lucide-react'
import { useState } from 'react'
-import ReportChart from 'components/interfaces/Reports/ReportChart'
+import { ReportChart } from 'components/interfaces/Reports/ReportChart'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import ReportStickyNav from 'components/interfaces/Reports/ReportStickyNav'
diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx
index 48ab74b916f..959967b54aa 100644
--- a/apps/studio/pages/project/[ref]/reports/database.tsx
+++ b/apps/studio/pages/project/[ref]/reports/database.tsx
@@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useFlag, useParams } from 'common'
-import ReportChart from 'components/interfaces/Reports/ReportChart'
+import { ReportChart } from 'components/interfaces/Reports/ReportChart'
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants'
@@ -22,7 +22,7 @@ import Table from 'components/to-be-cleaned/Table'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import ChartHandler from 'components/ui/Charts/ChartHandler'
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
-import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
+import { LazyComposedChartHandler } from 'components/ui/Charts/ComposedChartHandler'
import { ReportSettings } from 'components/ui/Charts/ReportSettings'
import GrafanaPromoBanner from 'components/ui/GrafanaPromoBanner'
import Panel from 'components/ui/Panel'
@@ -269,7 +269,7 @@ const DatabaseUsage = () => {
orgPlan?.id &&
(showChartsV2
? REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => (
-
{
))
: REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart, i) =>
chart.availableIn?.includes(orgPlan?.id) ? (
- {
{
+const EdgeFunctionsReportV2: NextPageWithLayout = () => {
return (
@@ -32,20 +34,23 @@ const EdgeFunctionsReport: NextPageWithLayout = () => {
)
}
-EdgeFunctionsReport.getLayout = (page) => (
+EdgeFunctionsReportV2.getLayout = (page) => (
{page}
)
-export type UpdateDateRange = (from: string, to: string) => void
-export default EdgeFunctionsReport
+export default EdgeFunctionsReportV2
const EdgeFunctionsUsage = () => {
const { ref } = useParams()
const { data: functions, isLoading: isLoadingFunctions } = useEdgeFunctionsQuery({
projectRef: ref,
})
+
+ const chartSyncId = `edge-functions-${ref}`
+ useChartHoverState(chartSyncId)
+
const [isOpen, setIsOpen] = useState(false)
const [functionIds, setFunctionIds] = useState([])
const [tempFunctionIds, setTempFunctionIds] = useState(functionIds)
@@ -61,8 +66,6 @@ const EdgeFunctionsUsage = () => {
updateDateRange,
datePickerValue,
datePickerHelpers,
- isOrgPlanLoading,
- orgPlan,
showUpgradePrompt,
setShowUpgradePrompt,
handleDatePickerChange,
@@ -71,21 +74,27 @@ const EdgeFunctionsUsage = () => {
const queryClient = useQueryClient()
const [isRefreshing, setIsRefreshing] = useState(false)
- const EDGEFN_CHARTS = getEdgeFunctionReportAttributes()
+ const reportConfig = useMemo(() => {
+ return edgeFunctionReports({
+ projectRef: ref!,
+ functions: functions ?? [],
+ startDate: selectedDateRange?.period_start?.date ?? '',
+ endDate: selectedDateRange?.period_end?.date ?? '',
+ interval: selectedDateRange?.interval ?? 'minute',
+ filters: {
+ functionIds,
+ },
+ })
+ }, [ref, functions, selectedDateRange, functionIds])
const onRefreshReport = async () => {
if (!selectedDateRange) return
setIsRefreshing(true)
- queryClient.invalidateQueries(['edge-function-report', ref])
+ queryClient.invalidateQueries(['report-v2'])
setTimeout(() => setIsRefreshing(false), 1000)
}
- if (!ref) {
- // Prevent rendering charts until the ref is available
- return <>>
- }
-
return (
<>
@@ -101,6 +110,8 @@ const EdgeFunctionsUsage = () => {
tooltip={{ content: { side: 'bottom', text: 'Refresh report' } }}
onClick={onRefreshReport}
/>
+
+
{
>
{selectedDateRange &&
- EDGEFN_CHARTS.filter((attr) => !attr.hide).map((attr, i) => (
-
- ))}
+ reportConfig
+ .filter((report) => !report.hide)
+ .map((report) => (
+
+ ))}
>
diff --git a/apps/studio/pages/project/[ref]/reports/realtime.tsx b/apps/studio/pages/project/[ref]/reports/realtime.tsx
index 27b2c81abce..14ef2d580bd 100644
--- a/apps/studio/pages/project/[ref]/reports/realtime.tsx
+++ b/apps/studio/pages/project/[ref]/reports/realtime.tsx
@@ -19,7 +19,7 @@ import {
TopApiRoutesRenderer,
TotalRequestsChartRenderer,
} from 'components/interfaces/Reports/renderers/ApiRenderers'
-import ComposedChartHandler from 'components/ui/Charts/ComposedChartHandler'
+import { LazyComposedChartHandler } from 'components/ui/Charts/ComposedChartHandler'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import ReportFilterBar from 'components/interfaces/Reports/ReportFilterBar'
@@ -187,7 +187,7 @@ const RealtimeUsage = () => {
>
{selectedDateRange &&
REALTIME_REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => (
-
- setRequestStatus: (pipelineId: number, status: PipelineStatusRequestStatus) => void
+ pipelineStatusSnapshot: Record
+ setRequestStatus: (
+ pipelineId: number,
+ status: PipelineStatusRequestStatus,
+ snapshotStatus?: string
+ ) => void
getRequestStatus: (pipelineId: number) => PipelineStatusRequestStatus
updatePipelineStatus: (pipelineId: number, backendStatus: string | undefined) => void
}
@@ -25,12 +39,58 @@ export const PipelineRequestStatusProvider = ({ children }: PipelineRequestStatu
const [requestStatus, setRequestStatusState] = useState<
Record
>({})
+ const [pipelineStatusSnapshot, setPipelineStatusSnapshot] = useState<
+ Record
+ >({})
+ const timeoutsRef = useRef>({})
+ const REQUEST_TIMEOUT_MS = 10_000
- const setRequestStatus = (pipelineId: number, status: PipelineStatusRequestStatus) => {
+ const setRequestStatus = (
+ pipelineId: number,
+ status: PipelineStatusRequestStatus,
+ snapshotStatus?: string
+ ) => {
setRequestStatusState((prev) => ({
...prev,
[pipelineId]: status,
}))
+ setPipelineStatusSnapshot((prev) => {
+ if (status === PipelineStatusRequestStatus.None) {
+ const { [pipelineId]: _omit, ...rest } = prev
+ return rest
+ }
+ // Only set snapshot when provided to avoid undefined entries
+ if (snapshotStatus !== undefined) {
+ return { ...prev, [pipelineId]: snapshotStatus }
+ }
+ return prev
+ })
+
+ // Clear existing timeout for this pipeline
+ const existing = timeoutsRef.current[pipelineId]
+ if (existing !== undefined) {
+ clearTimeout(existing)
+ delete timeoutsRef.current[pipelineId]
+ }
+
+ // Start auto-reset timer for non-None states
+ if (status !== PipelineStatusRequestStatus.None) {
+ const id = window.setTimeout(() => {
+ // If still pending, clear to None to show backend state
+ setRequestStatusState((prev) => {
+ if (prev[pipelineId] && prev[pipelineId] !== PipelineStatusRequestStatus.None) {
+ return { ...prev, [pipelineId]: PipelineStatusRequestStatus.None }
+ }
+ return prev
+ })
+ setPipelineStatusSnapshot((prev) => {
+ const { [pipelineId]: _omit, ...rest } = prev
+ return rest
+ })
+ delete timeoutsRef.current[pipelineId]
+ }, REQUEST_TIMEOUT_MS)
+ timeoutsRef.current[pipelineId] = id
+ }
}
const getRequestStatus = (pipelineId: number): PipelineStatusRequestStatus => {
@@ -38,25 +98,32 @@ export const PipelineRequestStatusProvider = ({ children }: PipelineRequestStatu
}
const updatePipelineStatus = useCallback(
- (pipelineId: number, backendStatus: string | undefined) => {
+ (pipelineId: number, newStatus: string | undefined) => {
const currentRequestStatus = requestStatus[pipelineId] || PipelineStatusRequestStatus.None
+ if (currentRequestStatus === PipelineStatusRequestStatus.None) return
- if (
- (currentRequestStatus === PipelineStatusRequestStatus.EnableRequested &&
- (backendStatus === 'started' || backendStatus === 'failed')) ||
- (currentRequestStatus === PipelineStatusRequestStatus.DisableRequested &&
- (backendStatus === 'stopped' || backendStatus === 'failed'))
- ) {
+ // Only remove when backend status differs from snapshot
+ const snapshotStatus = pipelineStatusSnapshot[pipelineId]
+ if (newStatus !== snapshotStatus) {
setRequestStatus(pipelineId, PipelineStatusRequestStatus.None)
}
},
- [requestStatus, setRequestStatus]
+ [requestStatus, pipelineStatusSnapshot]
)
+ // Cleanup all timers on unmount
+ useEffect(() => {
+ return () => {
+ Object.values(timeoutsRef.current).forEach((id) => clearTimeout(id))
+ timeoutsRef.current = {}
+ }
+ }, [])
+
return (
item.status !== STORAGE_ROW_STATUS.EDITING
)
+ // [Joshen] JFYI storage does support folders of the same name with different casing
+ // but its an issue with the List V1 endpoint that's causing an issue with fetching contents
+ // for folders of the same name with different casing
+ // We should remove this check once all projects are on the List V2 endpoint
const hasSameNameInColumn =
currentColumnItems.filter((item) => item.name.toLowerCase() === name.toLowerCase()).length >
0
diff --git a/apps/studio/tests/features/logs/Logs.utils.test.ts b/apps/studio/tests/features/logs/Logs.utils.test.ts
index b074f865b8a..ace442b6142 100644
--- a/apps/studio/tests/features/logs/Logs.utils.test.ts
+++ b/apps/studio/tests/features/logs/Logs.utils.test.ts
@@ -2,8 +2,235 @@ import {
checkForILIKEClause,
checkForWildcard,
checkForWithClause,
+ fillTimeseries,
+ unixMicroToIsoTimestamp,
} from 'components/interfaces/Settings/Logs/Logs.utils'
import { describe, test, expect } from 'vitest'
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+
+dayjs.extend(utc)
+
+describe('fillTimeseries', () => {
+ test('should return empty array for empty data without min/max', () => {
+ const result = fillTimeseries([], 'timestamp', 'value', 0)
+ expect(result).toEqual([])
+ })
+
+ test('should return empty array for empty data with min/max', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T01:00:00.000Z'
+ const result = fillTimeseries([], 'timestamp', 'value', 0, min, max)
+
+ // When min/max are provided, the function fills the time range with default values
+ // This creates 61 data points (one for each minute from 00:00 to 01:00)
+ expect(result).toHaveLength(61)
+ expect(result[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
+ expect(result[60]).toEqual({ timestamp: '2023-01-01T01:00:00.000Z', value: 0 })
+ })
+
+ test('should normalize timestamps when data exceeds minPointsToFill', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00.000Z', value: 1 },
+ { timestamp: '2023-01-01T00:01:00.000Z', value: 2 },
+ { timestamp: '2023-01-01T00:02:00.000Z', value: 3 },
+ { timestamp: '2023-01-01T00:03:00.000Z', value: 4 },
+ { timestamp: '2023-01-01T00:04:00.000Z', value: 5 },
+ { timestamp: '2023-01-01T00:05:00.000Z', value: 6 },
+ { timestamp: '2023-01-01T00:06:00.000Z', value: 7 },
+ { timestamp: '2023-01-01T00:07:00.000Z', value: 8 },
+ { timestamp: '2023-01-01T00:08:00.000Z', value: 9 },
+ { timestamp: '2023-01-01T00:09:00.000Z', value: 10 },
+ { timestamp: '2023-01-01T00:10:00.000Z', value: 11 },
+ { timestamp: '2023-01-01T00:11:00.000Z', value: 12 },
+ { timestamp: '2023-01-01T00:12:00.000Z', value: 13 },
+ { timestamp: '2023-01-01T00:13:00.000Z', value: 14 },
+ { timestamp: '2023-01-01T00:14:00.000Z', value: 15 },
+ { timestamp: '2023-01-01T00:15:00.000Z', value: 16 },
+ { timestamp: '2023-01-01T00:16:00.000Z', value: 17 },
+ { timestamp: '2023-01-01T00:17:00.000Z', value: 18 },
+ { timestamp: '2023-01-01T00:18:00.000Z', value: 19 },
+ { timestamp: '2023-01-01T00:19:00.000Z', value: 20 },
+ { timestamp: '2023-01-01T00:20:00.000Z', value: 21 },
+ ]
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 20)
+
+ // Should return normalized data without filling gaps
+ expect(result).toHaveLength(21)
+ result.forEach((item) => {
+ expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
+ })
+ })
+
+ test('should fill gaps in sparse data with 1-minute intervals', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:04:00.000Z'
+ const data = [
+ { timestamp: '2023-01-01T00:01:00.000Z', value: 10 },
+ { timestamp: '2023-01-01T00:03:00.000Z', value: 30 },
+ ]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '2m')
+
+ expect(result).toHaveLength(5)
+ const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
+
+ expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
+ expect(sortedResult[1]).toEqual({ timestamp: '2023-01-01T00:01:00.000Z', value: 10 })
+ expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', value: 0 })
+ expect(sortedResult[3]).toEqual({ timestamp: '2023-01-01T00:03:00.000Z', value: 30 })
+ expect(sortedResult[4]).toEqual({ timestamp: '2023-01-01T00:04:00.000Z', value: 0 })
+ })
+
+ test('should handle multiple value keys', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:02:00.000Z'
+ const data = [{ timestamp: '2023-01-01T00:01:00.000Z', count1: 10, count2: 100 }]
+
+ const result = fillTimeseries(data, 'timestamp', ['count1', 'count2'], 5, min, max, 20, '1m')
+
+ expect(result).toHaveLength(3)
+ const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
+
+ expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', count1: 5, count2: 5 })
+ expect(sortedResult[1]).toEqual({
+ timestamp: '2023-01-01T00:01:00.000Z',
+ count1: 10,
+ count2: 100,
+ })
+ expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', count1: 5, count2: 5 })
+ })
+
+ test('should handle different interval formats', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:10:00.000Z'
+ const data = [{ timestamp: '2023-01-01T00:05:00.000Z', value: 50 }]
+
+ // Test 5-minute intervals: 00:00, 00:05, 00:10 = 3 points
+ const result5m = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '5m')
+ expect(result5m).toHaveLength(3)
+
+ // Test 2-minute intervals: 00:00, 00:02, 00:04, 00:05, 00:06, 00:08, 00:10 = 7 points
+ const result2m = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '2m')
+ console.log(
+ '2m intervals:',
+ result2m.map((r) => r.timestamp)
+ )
+ expect(result2m).toHaveLength(7)
+
+ // Test 1-hour intervals: 00:00, 01:00, 02:00 with existing data at 00:05 = 4 points
+ const maxHour = '2023-01-01T02:00:00.000Z'
+ const result1h = fillTimeseries(data, 'timestamp', 'value', 0, min, maxHour, 20, '1h')
+ expect(result1h).toHaveLength(4)
+ })
+
+ test('should handle microsecond timestamps correctly', () => {
+ const now = dayjs.utc('2023-01-01T00:00:00.000Z')
+ const data = [
+ { timestamp: now.valueOf() * 1000, value: 1 },
+ { timestamp: now.add(1, 'minute').valueOf() * 1000, value: 2 },
+ ]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 1)
+
+ expect(result).toHaveLength(2)
+ result.forEach((item) => {
+ expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
+ })
+ })
+
+ test('should handle mixed timestamp formats', () => {
+ const data = [
+ { timestamp: '2023-01-01T00:00:00.000Z', value: 1 },
+ { timestamp: dayjs.utc('2023-01-01T00:01:00.000Z').valueOf() * 1000, value: 2 },
+ { timestamp: '2023-01-01T00:02:00.000Z', value: 3 },
+ ]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 1)
+
+ expect(result).toHaveLength(3)
+ result.forEach((item) => {
+ expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
+ })
+ })
+
+ test('should not fill gaps when data is dense enough', () => {
+ const data = Array.from({ length: 25 }, (_, i) => ({
+ timestamp: dayjs.utc('2023-01-01T00:00:00.000Z').add(i, 'minute').toISOString(),
+ value: i,
+ }))
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 20)
+
+ expect(result).toHaveLength(25)
+ expect(result).toEqual(data)
+ })
+
+ test('should handle edge case with single data point', () => {
+ const data = [{ timestamp: '2023-01-01T00:00:00.000Z', value: 1 }]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0)
+
+ expect(result).toEqual(data)
+ })
+
+ test('should handle edge case with single data point and min/max', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:02:00.000Z'
+ const data = [{ timestamp: '2023-01-01T00:01:00.000Z', value: 1 }]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '1m')
+
+ expect(result).toHaveLength(3)
+ const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
+
+ expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
+ expect(sortedResult[1]).toEqual({ timestamp: '2023-01-01T00:01:00.000Z', value: 1 })
+ expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', value: 0 })
+ })
+
+ test('should handle invalid interval format gracefully', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:02:00.000Z'
+ const data = [{ timestamp: '2023-01-01T00:01:00.000Z', value: 1 }]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, 'invalid')
+
+ // Should fall back to default behavior
+ expect(result.length).toBeGreaterThan(1)
+ })
+
+ test('should preserve all properties of original data', () => {
+ const data = [
+ {
+ timestamp: '2023-01-01T00:00:00.000Z',
+ value: 1,
+ extra: 'data',
+ nested: { prop: 'value' },
+ },
+ ]
+
+ const result = fillTimeseries(data, 'timestamp', 'value', 0)
+
+ expect(result[0]).toEqual(data[0])
+ expect(result[0].extra).toBe('data')
+ expect(result[0].nested).toEqual({ prop: 'value' })
+ })
+
+ test('should handle empty value keys array', () => {
+ const min = '2023-01-01T00:00:00.000Z'
+ const max = '2023-01-01T00:01:00.000Z'
+ const data = [{ timestamp: '2023-01-01T00:00:30.000Z', value: 1 }]
+
+ const result = fillTimeseries(data, 'timestamp', [], 0, min, max, 20, '30s')
+
+ expect(result).toHaveLength(3)
+ result.forEach((item) => {
+ expect(item).toHaveProperty('timestamp')
+ expect(item).not.toHaveProperty('value')
+ })
+ })
+})
describe('checkForWithClause', () => {
test('basic queries', () => {
diff --git a/apps/studio/tests/vitestSetup.ts b/apps/studio/tests/vitestSetup.ts
index f309589b528..df9816a4bba 100644
--- a/apps/studio/tests/vitestSetup.ts
+++ b/apps/studio/tests/vitestSetup.ts
@@ -4,6 +4,14 @@ import { createDynamicRouteParser } from 'next-router-mock/dist/dynamic-routes'
import { afterAll, afterEach, beforeAll, vi } from 'vitest'
import { routerMock } from './lib/route-mock'
import { mswServer } from './lib/msw'
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(relativeTime)
// Uncomment this if HTML in errors are being annoying.
//
@@ -38,7 +46,7 @@ beforeAll(() => {
vi.mock('next/compat/router', () => require('next-router-mock'))
// Mock the useParams hook from common module globally
- vi.mock('common', async (importOriginal) => {
+ vi.mock('common', async (importOriginal: any) => {
const actual = await importOriginal()
return {
...(typeof actual === 'object' ? actual : {}),
diff --git a/apps/www/_events/2025-09-10-migrating-from-firebase-mobbin.mdx b/apps/www/_events/2025-09-10-migrating-from-firebase-mobbin.mdx
new file mode 100644
index 00000000000..5859478796c
--- /dev/null
+++ b/apps/www/_events/2025-09-10-migrating-from-firebase-mobbin.mdx
@@ -0,0 +1,45 @@
+---
+title: 'Firebase to Postgres: Breaking through the NoSQL ceiling with Mobbin'
+meta_title: 'Firebase to Postgres: Breaking through the NoSQL ceiling with Mobbin'
+subtitle: 'How Mobbin scaled beyond Firebase with Supabase and Postgres'
+meta_description: 'Learn how to overcome Firebase limitations and migrate to Postgres with Supabase for better performance, scalability, and cost control.'
+type: 'webinar'
+onDemand: true
+date: '2025-09-10T08:00:00.000-07:00'
+timezone: 'America/Los_Angeles'
+duration: '45 mins'
+company:
+ {
+ name: 'Mobbin',
+ website_url: 'https://mobbin.com/',
+ logo: '/images/customers/logos/light/mobbin.png',
+ logo_light: '/images/customers/logos/mobbin.png',
+ }
+categories:
+ - webinar
+main_cta: {
+ url: 'https://zoom.us/webinar/register/WN_cMSVDLRSRKS5cH8EmmUUQg', # Replace with actual registration link
+ target: '_blank',
+ label: 'Register now',
+ }
+speakers: 'dventimi,jian_jie'
+---
+
+## Firebase to Postgres: Breaking through the NoSQL ceiling
+
+Firebase is great for MVPs, but most teams hit the ceiling fast. When your app starts to scale, Firestore’s NoSQL model can slow you down, limit your queries, and drive up costs without warning.
+
+In this 45-minute session, Supabase engineers and Mobbin CEO Liau Jian Jie break down the technical challenges of growing on Firebase and show how Postgres gives you the tools to scale with confidence. You’ll learn how to migrate your app to Supabase on AWS using modern SQL, real-time features, and flexible auth and storage, all with better performance and cost control.
+
+This session is built for startup founders and technical leads who want to stay fast without giving up control.
+
+### Key takeaways
+
+- What breaks when Firebase apps start to scale
+- How SQL makes complex queries simple
+- How to migrate data, auth, and storage in a weekend
+- Why Postgres improves speed and observability
+- What it really costs to stay on Firebase
+- Lessons from a founder who already made the switch
+
+Join us live to participate in the Q&A afterwards. Can’t make it to the event? We’ll send you a link to the recording.
diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx
index 7146986b559..94d27bf0582 100644
--- a/apps/www/components/Hero/Hero.tsx
+++ b/apps/www/components/Hero/Hero.tsx
@@ -3,8 +3,6 @@ import Link from 'next/link'
import { Button } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import { useSendTelemetryEvent } from '~/lib/telemetry'
-import AnnouncementBadge from '../Announcement/Badge'
-import { announcement } from 'ui-patterns'
const Hero = () => {
const sendTelemetryEvent = useSendTelemetryEvent()
diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx
index 8746eb592b9..c9232b6bc05 100644
--- a/apps/www/components/Nav/index.tsx
+++ b/apps/www/components/Nav/index.tsx
@@ -9,10 +9,10 @@ import { useIsLoggedIn, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import { AuthenticatedDropdownMenu } from 'ui-patterns'
+import { useSendTelemetryEvent } from 'lib/telemetry'
import GitHubButton from './GitHubButton'
import HamburgerButton from './HamburgerMenu'
import RightClickBrandLogo from './RightClickBrandLogo'
-import { useSendTelemetryEvent } from 'lib/telemetry'
import useDropdownMenu from './useDropdownMenu'
import { getMenu } from 'data/nav'
diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json
index ab0e0d3d741..faa1a33114a 100644
--- a/apps/www/lib/authors.json
+++ b/apps/www/lib/authors.json
@@ -653,5 +653,21 @@
"position": "Security Engineer",
"author_url": "https://github.com/staaldraad",
"author_image_url": "https://avatars.githubusercontent.com/u/4200832?v=4"
+ },
+ {
+ "author_id": "dventimi",
+ "author": "David Ventimiglia",
+ "username": "dventimi",
+ "position": "Engineering",
+ "author_url": "https://github.com/dventimi",
+ "author_image_url": "https://avatars.githubusercontent.com/u/97983663?v=4"
+ },
+ {
+ "author_id": "jian_jie",
+ "author": "Jian Jie Liau",
+ "username": "jianji",
+ "position": "CTO and Co-Founder Mobbin",
+ "author_url": "https://github.com/liaujianjie",
+ "author_image_url": "https://avatars.githubusercontent.com/u/3143132?v=4"
}
]
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx
index f033817bc8a..be7a9e4042e 100644
--- a/apps/www/pages/_app.tsx
+++ b/apps/www/pages/_app.tsx
@@ -5,11 +5,12 @@ import '../styles/index.css'
import {
AuthProvider,
FeatureFlagProvider,
+ getFlags as getConfigCatFlags,
IS_PLATFORM,
PageTelemetry,
+ TelemetryTagManager,
ThemeProvider,
useThemeSandbox,
- TelemetryTagManager,
} from 'common'
import { DefaultSeo } from 'next-seo'
import { AppProps } from 'next/app'
@@ -86,7 +87,7 @@ export default function App({ Component, pageProps }: AppProps) {
/>
-
+
theme.value)}
enableSystem
diff --git a/apps/www/public/images/customers/logos/light/mobbin.png b/apps/www/public/images/customers/logos/light/mobbin.png
index 49cfe40567d..4432df06a21 100644
Binary files a/apps/www/public/images/customers/logos/light/mobbin.png and b/apps/www/public/images/customers/logos/light/mobbin.png differ
diff --git a/apps/www/public/images/customers/logos/mobbin.png b/apps/www/public/images/customers/logos/mobbin.png
index 568732a089d..ab831e6b687 100644
Binary files a/apps/www/public/images/customers/logos/mobbin.png and b/apps/www/public/images/customers/logos/mobbin.png differ
diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts
index 1c2b7042faa..87bbc37c9df 100644
--- a/packages/api-types/types/api.d.ts
+++ b/packages/api-types/types/api.d.ts
@@ -337,6 +337,23 @@ export interface paths {
patch?: never
trace?: never
}
+ '/v1/projects/{ref}/analytics/endpoints/functions.combined-stats': {
+ parameters: {
+ query?: never
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ /** Gets a project's function combined statistics */
+ get: operations['v1-get-project-function-combined-stats']
+ put?: never
+ post?: never
+ delete?: never
+ options?: never
+ head?: never
+ patch?: never
+ trace?: never
+ }
'/v1/projects/{ref}/analytics/endpoints/logs.all': {
parameters: {
query?: never
@@ -946,7 +963,11 @@ export interface paths {
* @description Modifies the roles that can be assumed and for how long
*/
put: operations['v1-update-jit-access']
- post?: never
+ /**
+ * Authorize user-id to role mappings for JIT access
+ * @description Authorizes the request to assume a role in the project database
+ */
+ post: operations['v1-authorize-jit-access']
delete?: never
options?: never
head?: never
@@ -1828,6 +1849,8 @@ export interface components {
mfa_totp_verify_enabled: boolean | null
mfa_web_authn_enroll_enabled: boolean | null
mfa_web_authn_verify_enabled: boolean | null
+ nimbus_oauth_client_id: string | null
+ nimbus_oauth_client_secret: string | null
password_hibp_enabled: boolean | null
password_min_length: number | null
/** @enum {string|null} */
@@ -1894,6 +1917,10 @@ export interface components {
smtp_user: string | null
uri_allow_list: string | null
}
+ AuthorizeJitAccessBody: {
+ rhost: string
+ role: string
+ }
BranchActionBody: {
migration_version?: string
}
@@ -1959,6 +1986,7 @@ export interface components {
| 'FUNCTIONS_FAILED'
/** Format: date-time */
updated_at: string
+ with_data: boolean
}
BranchUpdateResponse: {
/** @enum {string} */
@@ -2336,16 +2364,48 @@ export interface components {
/** Format: uuid */
user_id: string
user_roles: {
- expires_at?: string
+ allowed_networks?: {
+ allowed_cidrs?: {
+ cidr: string
+ }[]
+ allowed_cidrs_v6?: {
+ cidr: string
+ }[]
+ }
+ expires_at?: number
role: string
}[]
}
+ JitAuthorizeAccessResponse: {
+ /** Format: uuid */
+ user_id: string
+ user_role: {
+ allowed_networks?: {
+ allowed_cidrs?: {
+ cidr: string
+ }[]
+ allowed_cidrs_v6?: {
+ cidr: string
+ }[]
+ }
+ expires_at?: number
+ role: string
+ }
+ }
JitListAccessResponse: {
items: {
/** Format: uuid */
user_id: string
user_roles: {
- expires_at?: string
+ allowed_networks?: {
+ allowed_cidrs?: {
+ cidr: string
+ }[]
+ allowed_cidrs_v6?: {
+ cidr: string
+ }[]
+ }
+ expires_at?: number
role: string
}[]
}[]
@@ -2981,6 +3041,8 @@ export interface components {
mfa_totp_verify_enabled?: boolean | null
mfa_web_authn_enroll_enabled?: boolean | null
mfa_web_authn_verify_enabled?: boolean | null
+ nimbus_oauth_client_id?: string | null
+ nimbus_oauth_client_secret?: string | null
password_hibp_enabled?: boolean | null
password_min_length?: number | null
/** @enum {string|null} */
@@ -3107,7 +3169,15 @@ export interface components {
}
UpdateJitAccessBody: {
roles: {
- expires_at?: string
+ allowed_networks?: {
+ allowed_cidrs?: {
+ cidr: string
+ }[]
+ allowed_cidrs_v6?: {
+ cidr: string
+ }[]
+ }
+ expires_at?: number
role: string
}[]
/** Format: uuid */
@@ -4283,6 +4353,44 @@ export interface operations {
}
}
}
+ 'v1-get-project-function-combined-stats': {
+ parameters: {
+ query: {
+ function_id: string
+ interval: '15min' | '1hr' | '3hr' | '1day'
+ }
+ header?: never
+ path: {
+ /** @description Project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['AnalyticsResponse']
+ }
+ }
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to get project's function combined statistics */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
'v1-get-project-logs': {
parameters: {
query?: {
@@ -4692,7 +4800,30 @@ export interface operations {
query?: never
header?: never
path: {
- addon_variant: unknown
+ addon_variant:
+ | (
+ | 'ci_micro'
+ | 'ci_small'
+ | 'ci_medium'
+ | 'ci_large'
+ | 'ci_xlarge'
+ | 'ci_2xlarge'
+ | 'ci_4xlarge'
+ | 'ci_8xlarge'
+ | 'ci_12xlarge'
+ | 'ci_16xlarge'
+ | 'ci_24xlarge'
+ | 'ci_24xlarge_optimized_cpu'
+ | 'ci_24xlarge_optimized_memory'
+ | 'ci_24xlarge_high_memory'
+ | 'ci_48xlarge'
+ | 'ci_48xlarge_optimized_cpu'
+ | 'ci_48xlarge_optimized_memory'
+ | 'ci_48xlarge_high_memory'
+ )
+ | 'cd_default'
+ | ('pitr_7' | 'pitr_14' | 'pitr_28')
+ | 'ipv4_default'
/** @description Project ref */
ref: string
}
@@ -6229,6 +6360,45 @@ export interface operations {
}
}
}
+ 'v1-authorize-jit-access': {
+ parameters: {
+ query?: never
+ header?: never
+ path: {
+ /** @description Project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['AuthorizeJitAccessBody']
+ }
+ }
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['JitAuthorizeAccessResponse']
+ }
+ }
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to authorize database jit access */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
'v1-delete-jit-access': {
parameters: {
query?: never
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 70b04347348..c6fb1f2f00d 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -458,7 +458,7 @@ export interface paths {
}
get?: never
put?: never
- /** Send exit survey to HubSpot, Notion, and survey_responses table */
+ /** Send exit survey to HubSpot and survey_responses table */
post: operations['SendFeedbackController_sendExitSurvey']
delete?: never
options?: never
@@ -801,6 +801,23 @@ export interface paths {
patch?: never
trace?: never
}
+ '/platform/integrations/vercel/connections/project/{project_ref}': {
+ parameters: {
+ query?: never
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ /** Gets all Vercel integrations (regular and marketplace) with their connections for a given project */
+ get: operations['VercelConnectionsController_getProjectVercelConnections']
+ put?: never
+ post?: never
+ delete?: never
+ options?: never
+ head?: never
+ patch?: never
+ trace?: never
+ }
'/platform/integrations/vercel/projects/{organization_integration_id}': {
parameters: {
query?: never
@@ -2134,23 +2151,6 @@ export interface paths {
patch?: never
trace?: never
}
- '/platform/profile/search': {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- get?: never
- put?: never
- /** Search profiles by username, email with the given keywords */
- post: operations['SearchProfileController_searchProfile']
- delete?: never
- options?: never
- head?: never
- patch?: never
- trace?: never
- }
'/platform/projects': {
parameters: {
query?: never
@@ -2211,15 +2211,15 @@ export interface paths {
patch: operations['ProjectsRefController_updateProject']
trace?: never
}
- '/platform/projects/{ref}/analytics/endpoints/functions.inv-stats': {
+ '/platform/projects/{ref}/analytics/endpoints/functions.combined-stats': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
- /** Gets a project's function invocation statistics */
- get: operations['FunctionInvocationLogsController_getStatus']
+ /** Gets a project's function combined statistics */
+ get: operations['FunctionsLogsController_getCombinedStats']
put?: never
post?: never
delete?: never
@@ -2236,7 +2236,7 @@ export interface paths {
cookie?: never
}
/** Gets a project's function request statistics */
- get: operations['FunctionRequestLogsController_getStatus']
+ get: operations['FunctionsLogsController_getRequestStats']
put?: never
post?: never
delete?: never
@@ -2253,7 +2253,7 @@ export interface paths {
cookie?: never
}
/** Gets a project's function resource usage */
- get: operations['FunctionResourceLogsController_getStatus']
+ get: operations['FunctionsLogsController_getResourceUsage']
put?: never
post?: never
delete?: never
@@ -4206,58 +4206,6 @@ export interface paths {
patch?: never
trace?: never
}
- '/platform/vercel/projects': {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- /** Gets the list of Vercel projects */
- get: operations['VercelProjectsController_getVercelProjects']
- put?: never
- post?: never
- delete?: never
- options?: never
- head?: never
- patch?: never
- trace?: never
- }
- '/platform/vercel/projects/{id}': {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- /** Gets the Vercel project with the given ID */
- get: operations['VercelProjectsController_getVercelProject']
- put?: never
- post?: never
- delete?: never
- options?: never
- head?: never
- patch?: never
- trace?: never
- }
- '/platform/vercel/projects/envs': {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- /** Gets the environment variables for the given project ID on behalf of the given team ID */
- get: operations['VercelEnvironmentVariablesController_getEnvironmentVariables']
- put?: never
- /** Creates the environment variable for the given project ID on behalf of the given team ID */
- post: operations['VercelEnvironmentVariablesController_createEnvironmentVariable']
- delete?: never
- options?: never
- head?: never
- patch?: never
- trace?: never
- }
'/platform/vercel/redirect/{installation_id}': {
parameters: {
query?: never
@@ -4483,11 +4431,9 @@ export interface components {
walg_enabled: boolean
}
CloneProject: {
- /** @default 0 */
cloneBackupId?: number
newDbPass: string
newProjectName: string
- /** @default 0 */
recoveryTimeTarget?: number
}
CloudMarketplaceOnboardingInfoResponse: {
@@ -5015,8 +4961,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -5032,8 +4980,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -5270,12 +5220,6 @@ export interface components {
}
organization_integration_id: string
}
- CreateVercelEnvironmentVariableBody: {
- key: string
- target: string[]
- type: string
- value: string
- }
CreateVercelIntegrationBody: {
code: string
configuration_id: string
@@ -5756,6 +5700,68 @@ export interface components {
iso_timestamp_start?: string
sql?: string
}
+ GetProjectVercelConnectionsResponse: {
+ integrations: {
+ added_by: {
+ primary_email: string
+ username: string
+ }
+ connections: {
+ added_by: {
+ primary_email: string
+ username: string
+ }
+ env_sync_targets: string[]
+ foreign_project_id: string
+ id: string
+ inserted_at: string
+ metadata: {
+ framework?: string | null
+ name: string
+ supabaseConfig: {
+ projectEnvVars: {
+ write: boolean
+ }
+ }
+ }
+ organization_integration_id: string
+ public_env_var_prefix: string
+ supabase_project_ref: string
+ updated_at: string
+ }[]
+ id: string
+ inserted_at: string
+ metadata: {
+ account: {
+ avatar: string | null
+ name: string
+ source: string
+ team_id: string | null
+ team_slug: string | null
+ type: string
+ }
+ configuration_id: string
+ }
+ organization_slug: string
+ updated_at: string
+ }[]
+ marketplace_integrations: {
+ connections: {
+ foreign_project_id: string
+ framework: string | null
+ id: string
+ inserted_at: string
+ marketplace_installation_id: string
+ name: string
+ updated_at: string
+ vercel_owner_id: string | null
+ }[]
+ id: string
+ inserted_at: string
+ organization_slug: string
+ updated_at: string
+ }[]
+ }
GetPublicUrlBody: {
options?: {
download?: boolean
@@ -6177,6 +6183,8 @@ export interface components {
MFA_TOTP_VERIFY_ENABLED: boolean
MFA_WEB_AUTHN_ENROLL_ENABLED: boolean
MFA_WEB_AUTHN_VERIFY_ENABLED: boolean
+ NIMBUS_OAUTH_CLIENT_ID: string | null
+ NIMBUS_OAUTH_CLIENT_SECRET: string | null
PASSWORD_HIBP_ENABLED: boolean
PASSWORD_MIN_LENGTH: number
PASSWORD_REQUIRED_CHARACTERS: string
@@ -7263,13 +7271,6 @@ export interface components {
enabled: boolean
}
}
- Profile: {
- first_name: string
- gotrue_id: string
- id: number
- last_name: string
- username: string
- }
ProfileResponse: {
auth0_id: string
disabled_features: (
@@ -7668,8 +7669,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -7691,8 +7694,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -8040,9 +8045,6 @@ export interface components {
disable_statement_timeout?: boolean
query: string
}
- SearchProfileBody: {
- keywords: string
- }
SendDocsFeedbackBody: {
feedback?: string
isHelpful: boolean
@@ -8698,6 +8700,8 @@ export interface components {
MFA_TOTP_VERIFY_ENABLED?: boolean | null
MFA_WEB_AUTHN_ENROLL_ENABLED?: boolean | null
MFA_WEB_AUTHN_VERIFY_ENABLED?: boolean | null
+ NIMBUS_OAUTH_CLIENT_ID?: string | null
+ NIMBUS_OAUTH_CLIENT_SECRET?: string | null
PASSWORD_HIBP_ENABLED?: boolean | null
PASSWORD_MIN_LENGTH?: number | null
/** @enum {string|null} */
@@ -8964,8 +8968,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -8981,8 +8987,10 @@ export interface components {
big_query: {
/** @description BigQuery dataset id */
dataset_id: string
+ /** @description Maximum number of concurrent streams when writing */
+ max_concurrent_streams?: number
/** @description Max staleness in minutes */
- max_staleness_mins: number
+ max_staleness_mins?: number
/** @description BigQuery project id */
project_id: string
/** @description BigQuery service account key */
@@ -10192,6 +10200,12 @@ export interface operations {
'application/json': components['schemas']['CloneBackupsResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to list available valid backups */
500: {
headers: {
@@ -10225,6 +10239,12 @@ export interface operations {
'application/json': components['schemas']['ProjectClonedResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to clone the current project */
500: {
headers: {
@@ -10254,6 +10274,12 @@ export interface operations {
'application/json': components['schemas']['ProjectClonedStatusResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve clone project status */
500: {
headers: {
@@ -11094,6 +11120,34 @@ export interface operations {
}
}
}
+ VercelConnectionsController_getProjectVercelConnections: {
+ parameters: {
+ query?: never
+ header?: never
+ path: {
+ project_ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['GetProjectVercelConnectionsResponse']
+ }
+ }
+ /** @description Failed to get Vercel integrations for the given project */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
VercelProjectController_getVercelProjects: {
parameters: {
query?: {
@@ -12343,6 +12397,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to remove organization member */
500: {
headers: {
@@ -12375,6 +12435,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to assign organization member with new role */
500: {
headers: {
@@ -12408,6 +12474,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to update organization member role */
500: {
headers: {
@@ -12437,6 +12509,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to remove organization member role */
500: {
headers: {
@@ -12537,6 +12615,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to delete organization invitation with given id */
500: {
headers: {
@@ -12625,6 +12709,12 @@ export interface operations {
'application/json': components['schemas']['MfaStatusResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to get organization MFA status */
500: {
headers: {
@@ -14662,6 +14752,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to format sql query */
500: {
headers: {
@@ -14695,6 +14791,12 @@ export interface operations {
'application/json': components['schemas']['ValidateQueryResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to validate sql query */
500: {
headers: {
@@ -15639,36 +15741,6 @@ export interface operations {
}
}
}
- SearchProfileController_searchProfile: {
- parameters: {
- query?: never
- header?: never
- path?: never
- cookie?: never
- }
- requestBody: {
- content: {
- 'application/json': components['schemas']['SearchProfileBody']
- }
- }
- responses: {
- 201: {
- headers: {
- [name: string]: unknown
- }
- content: {
- 'application/json': components['schemas']['Profile'][]
- }
- }
- /** @description Failed to search profiles with the given keywords */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
ProjectsController_getProjects: {
parameters: {
query?: never
@@ -15825,49 +15897,11 @@ export interface operations {
}
}
}
- FunctionInvocationLogsController_getStatus: {
+ FunctionsLogsController_getCombinedStats: {
parameters: {
query: {
function_id: string
- interval: '5min' | '15min' | '1hr' | '1day' | '7day'
- }
- header?: never
- path: {
- /** @description Project ref */
- ref: string
- }
- cookie?: never
- }
- requestBody?: never
- responses: {
- 200: {
- headers: {
- [name: string]: unknown
- }
- content: {
- 'application/json': components['schemas']['AnalyticsResponse']
- }
- }
- 403: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to get project's function invocation statistics */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
- FunctionRequestLogsController_getStatus: {
- parameters: {
- query: {
- function_id: string
- interval: '5min' | '15min' | '1hr' | '1day' | '7day'
+ interval: '15min' | '1hr' | '3hr' | '1day'
}
header?: never
path: {
@@ -15901,11 +15935,49 @@ export interface operations {
}
}
}
- FunctionResourceLogsController_getStatus: {
+ FunctionsLogsController_getRequestStats: {
parameters: {
query: {
function_id: string
- interval: '5min' | '15min' | '1hr' | '1day' | '7day'
+ interval: '15min' | '1hr' | '3hr' | '1day'
+ }
+ header?: never
+ path: {
+ /** @description Project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['AnalyticsResponse']
+ }
+ }
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to get project's function request statistics */
+ 500: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
+ FunctionsLogsController_getResourceUsage: {
+ parameters: {
+ query: {
+ function_id: string
+ interval: '15min' | '1hr' | '3hr' | '1day'
}
header?: never
path: {
@@ -16074,6 +16146,12 @@ export interface operations {
'application/json': components['schemas']['AnalyticsResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to get project's usage api requests count */
500: {
headers: {
@@ -17039,6 +17117,12 @@ export interface operations {
'application/json': components['schemas']['PgbouncerConfigResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve project's pgbouncer config */
500: {
headers: {
@@ -17107,6 +17191,12 @@ export interface operations {
'application/json': components['schemas']['PgbouncerStatusResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve project's pgbouncer status */
500: {
headers: {
@@ -17136,6 +17226,12 @@ export interface operations {
'application/json': components['schemas']['PostgresConfigResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve project's Postgres config */
500: {
headers: {
@@ -17376,6 +17472,12 @@ export interface operations {
'application/json': components['schemas']['GetJwtSecretUpdateStatus']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve JWT secret update status */
500: {
headers: {
@@ -17479,6 +17581,12 @@ export interface operations {
'application/json': components['schemas']['SupavisorConfigResponse'][]
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve project's supavisor config */
500: {
headers: {
@@ -17651,7 +17759,7 @@ export interface operations {
ContentController_deleteContents: {
parameters: {
query: {
- ids: string[]
+ ids: string
}
header?: never
path: {
@@ -17808,7 +17916,7 @@ export interface operations {
ContentFoldersController_DeleteFolder: {
parameters: {
query: {
- ids: string[]
+ ids: string
}
header?: never
path?: never
@@ -18074,6 +18182,12 @@ export interface operations {
'application/json': components['schemas']['DatabaseStatusResponse'][]
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to get statuses of databases of a project */
500: {
headers: {
@@ -18354,6 +18468,10 @@ export interface operations {
| 'realtime_channel_events'
| 'realtime_channel_presence_events'
| 'realtime_channel_db_events'
+ | 'realtime_authorization_rls_execution_time'
+ | 'realtime_payload_size'
+ | 'realtime_connected_clients'
+ | 'realtime_replication_connection_lag'
databaseIdentifier?: string
endDate: string
interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d'
@@ -18374,6 +18492,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to get project's usage metrics */
500: {
headers: {
@@ -18436,6 +18560,12 @@ export interface operations {
'application/json': components['schemas']['LoadBalancerDetailResponse'][]
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
}
}
ProjectAdvisorNotificationsController_listNotificationExceptions: {
@@ -18458,6 +18588,12 @@ export interface operations {
'application/json': components['schemas']['ListNotificationExceptionsResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve advisor notification exceptions */
500: {
headers: {
@@ -18491,7 +18627,13 @@ export interface operations {
'application/json': components['schemas']['CreateNotificationExceptionsResponse']
}
}
- /** @description Failed to creare advisor notification exceptions */
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ /** @description Failed to create advisor notification exceptions */
500: {
headers: {
[name: string]: unknown
@@ -18503,7 +18645,7 @@ export interface operations {
ProjectAdvisorNotificationsController_deleteNotificationExceptions: {
parameters: {
query: {
- ids: string[]
+ ids: string
}
header?: never
path: {
@@ -18520,6 +18662,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to delete advisor notification exceptions */
500: {
headers: {
@@ -18552,6 +18700,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to update advisor notification exceptions */
500: {
headers: {
@@ -18644,6 +18798,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to resize database disk */
500: {
headers: {
@@ -18712,6 +18872,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to restart given services */
500: {
headers: {
@@ -18991,6 +19157,12 @@ export interface operations {
'application/json': components['schemas']['ServiceVersions']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
}
}
SettingsController_getProjectSettings: {
@@ -19013,6 +19185,12 @@ export interface operations {
'application/json': components['schemas']['ProjectSettingsResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to retrieve project's settings */
500: {
headers: {
@@ -20242,6 +20420,12 @@ export interface operations {
'application/json': components['schemas']['GetArchiveResponse']
}
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to get project storage archive */
500: {
headers: {
@@ -20269,6 +20453,12 @@ export interface operations {
}
content?: never
}
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
/** @description Failed to create project storage archive */
500: {
headers: {
@@ -21267,126 +21457,6 @@ export interface operations {
}
}
}
- VercelProjectsController_getVercelProjects: {
- parameters: {
- query?: {
- teamId?: string
- }
- header: {
- vercel_authorization: string
- }
- path?: never
- cookie?: never
- }
- requestBody?: never
- responses: {
- 200: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to get projects */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
- VercelProjectsController_getVercelProject: {
- parameters: {
- query?: {
- teamId?: string
- }
- header: {
- vercel_authorization: string
- }
- path: {
- id: string
- }
- cookie?: never
- }
- requestBody?: never
- responses: {
- 200: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to get project */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
- VercelEnvironmentVariablesController_getEnvironmentVariables: {
- parameters: {
- query: {
- projectId: string
- teamId?: string
- }
- header: {
- vercel_authorization: string
- }
- path?: never
- cookie?: never
- }
- requestBody?: never
- responses: {
- 200: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to get Vercel environment variables */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
- VercelEnvironmentVariablesController_createEnvironmentVariable: {
- parameters: {
- query: {
- projectId: string
- teamId?: string
- }
- header: {
- vercel_authorization: string
- }
- path?: never
- cookie?: never
- }
- requestBody: {
- content: {
- 'application/json': components['schemas']['CreateVercelEnvironmentVariableBody']
- }
- }
- responses: {
- 201: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- /** @description Failed to create Vercel environment variables */
- 500: {
- headers: {
- [name: string]: unknown
- }
- content?: never
- }
- }
- }
VercelRedirectController_getRedirectUrl: {
parameters: {
query?: never
diff --git a/packages/common/configcat.ts b/packages/common/configcat.ts
index 534a3b0a77d..410f40addcc 100644
--- a/packages/common/configcat.ts
+++ b/packages/common/configcat.ts
@@ -3,6 +3,17 @@ import * as configcat from 'configcat-js'
let client: configcat.IConfigCatClient
const endpoint = '/configuration-files/configcat-proxy/frontend-v2/config_v6.json'
+/**
+ * To set up ConfigCat for another app
+ * - Declare `FeatureFlagProvider` at the _app level
+ * - Pass in `getFlags` as `getConfigCatFlags` into `FeatureFlagProvider`
+ * - [Joshen] Wondering if this should just be baked into FeatureFlagProvider, rather than passed as a prop
+ * - Ensure that your app has the `NEXT_PUBLIC_CONFIGCAT_PROXY_URL` env var
+ * - [Joshen] Wondering if we can just set a default value for each env var, so can skip setting up env var in Vercel
+ * - Verify that your flags are now loading by console logging `flagValues` in `FeatureFlagProvider`'s useEffect
+ * - Can now use ConfigCat feature flags with the `useFlag` hook
+ */
+
export const fetchHandler: typeof fetch = async (input, init) => {
try {
return await fetch(input, init)
@@ -16,10 +27,14 @@ export const fetchHandler: typeof fetch = async (input, init) => {
}
async function getClient() {
- if (client) {
- return client
+ if (!process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL) {
+ console.error(
+ 'Failed to get ConfigCat client: missing env var "NEXT_PUBLIC_CONFIGCAT_PROXY_URL"'
+ )
}
+ if (client) return client
+
const response = await fetchHandler(process.env.NEXT_PUBLIC_CONFIGCAT_PROXY_URL + endpoint)
const options = { pollIntervalSeconds: 7 * 60 } // 7 minutes
if (response.status !== 200) {
@@ -39,11 +54,14 @@ async function getClient() {
return client
}
-export async function getFlags(userEmail: string = '') {
- if (userEmail) {
- const client = await getClient()
- return client.getAllValuesAsync(new configcat.User(userEmail))
- }
+export async function getFlags(userEmail: string = '', customAttributes?: Record) {
+ const client = await getClient()
- return []
+ if (userEmail) {
+ return client.getAllValuesAsync(
+ new configcat.User(userEmail, undefined, undefined, customAttributes)
+ )
+ } else {
+ return client.getAllValuesAsync()
+ }
}
diff --git a/packages/shared-data/regions.ts b/packages/shared-data/regions.ts
index 27caf5b2659..d38d8db339d 100644
--- a/packages/shared-data/regions.ts
+++ b/packages/shared-data/regions.ts
@@ -1,4 +1,4 @@
-export type CloudProvider = 'FLY' | 'AWS' | 'AWS_K8S'
+export type CloudProvider = 'FLY' | 'AWS' | 'AWS_K8S' | 'AWS_NIMBUS'
export type Region = typeof AWS_REGIONS | typeof FLY_REGIONS
export const AWS_REGIONS = {