diff --git a/api-contracts/openapi/components/schemas/tenant.yaml b/api-contracts/openapi/components/schemas/tenant.yaml index d540c6fab4..493dbff449 100644 --- a/api-contracts/openapi/components/schemas/tenant.yaml +++ b/api-contracts/openapi/components/schemas/tenant.yaml @@ -20,6 +20,9 @@ Tenant: environment: $ref: "#/TenantEnvironment" description: The environment type of the tenant. + dataRetentionPeriod: + type: string + description: The data retention period for the tenant, e.g. 720h. required: - metadata - name diff --git a/api/v1/server/handlers/v1/events/get.go b/api/v1/server/handlers/v1/events/get.go index 9a8ff87208..73b249f56d 100644 --- a/api/v1/server/handlers/v1/events/get.go +++ b/api/v1/server/handlers/v1/events/get.go @@ -3,14 +3,24 @@ package eventsv1 import ( "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" + "github.com/hatchet-dev/hatchet/pkg/analytics" v1 "github.com/hatchet-dev/hatchet/pkg/repository" + "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" ) func (t *V1EventsService) V1EventGet(ctx echo.Context, request gen.V1EventGetRequestObject) (gen.V1EventGetResponseObject, error) { + tenant := ctx.Get("tenant").(*sqlcv1.Tenant) event := ctx.Get("v1-event").(*v1.EventWithPayload) + if ts := event.EventSeenAt; ts.Valid && v1handlers.IsBeforeRetention(ts.Time, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx.Request().Context(), analytics.Event, analytics.Get, analytics.Properties{ + "outside_retention": true, + }) + } + return gen.V1EventGet200JSONResponse( transformers.ToV1Event(event), ), nil diff --git a/api/v1/server/handlers/v1/events/list.go b/api/v1/server/handlers/v1/events/list.go index f58eddb47f..ad1a9770de 100644 --- a/api/v1/server/handlers/v1/events/list.go +++ b/api/v1/server/handlers/v1/events/list.go @@ -9,8 +9,10 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" + "github.com/hatchet-dev/hatchet/pkg/analytics" "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" ) @@ -22,6 +24,16 @@ func (t *V1EventsService) V1EventList(ctx echo.Context, request gen.V1EventListR offset := int64(0) since := time.Now().Add(-time.Hour * 24) + if request.Params.Since != nil { + since = *request.Params.Since + } + + if v1handlers.IsBeforeRetention(since, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx.Request().Context(), analytics.Event, analytics.List, analytics.Properties{ + "outside_retention": true, + }) + } + if request.Params.Limit != nil { limit = *request.Params.Limit } @@ -30,10 +42,6 @@ func (t *V1EventsService) V1EventList(ctx echo.Context, request gen.V1EventListR offset = *request.Params.Offset } - if request.Params.Since != nil { - since = *request.Params.Since - } - opts := sqlcv1.ListEventsParams{ Tenantid: tenantId, Limit: pgtype.Int8{ diff --git a/api/v1/server/handlers/v1/logs/list.go b/api/v1/server/handlers/v1/logs/list.go index 72467163ca..1eeac9506d 100644 --- a/api/v1/server/handlers/v1/logs/list.go +++ b/api/v1/server/handlers/v1/logs/list.go @@ -6,13 +6,13 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + transformers "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" "github.com/hatchet-dev/hatchet/pkg/analytics" v1 "github.com/hatchet-dev/hatchet/pkg/repository" "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" "github.com/hatchet-dev/hatchet/pkg/telemetry" - - transformers "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" ) func (t *LogsService) V1TenantLogLineList(ctx echo.Context, request gen.V1TenantLogLineListRequestObject) (gen.V1TenantLogLineListResponseObject, error) { @@ -47,6 +47,12 @@ func (t *LogsService) V1TenantLogLineList(ctx echo.Context, request gen.V1Tenant since = request.Params.Since } + if since != nil && v1handlers.IsBeforeRetention(*since, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx.Request().Context(), analytics.Log, analytics.List, analytics.Properties{ + "outside_retention": true, + }) + } + if request.Params.Until != nil { until = request.Params.Until } diff --git a/api/v1/server/handlers/v1/retention.go b/api/v1/server/handlers/v1/retention.go new file mode 100644 index 0000000000..c85b047cfb --- /dev/null +++ b/api/v1/server/handlers/v1/retention.go @@ -0,0 +1,24 @@ +package v1 + +import "time" + +const defaultRetention = 720 * time.Hour + +func retentionBoundary(retentionPeriod string) time.Time { + retention, err := time.ParseDuration(retentionPeriod) + if err != nil || retention <= 0 { + retention = defaultRetention + } + + return time.Now().Add(-retention) +} + +// IsBeforeRetention returns true when the given timestamp is older than the +// tenant's retention window (now - retentionPeriod). +func IsBeforeRetention(t time.Time, retentionPeriod string) bool { + if t.IsZero() { + return false + } + + return t.Before(retentionBoundary(retentionPeriod)) +} diff --git a/api/v1/server/handlers/v1/tasks/get.go b/api/v1/server/handlers/v1/tasks/get.go index e49ce86946..07dcd16645 100644 --- a/api/v1/server/handlers/v1/tasks/get.go +++ b/api/v1/server/handlers/v1/tasks/get.go @@ -6,7 +6,9 @@ import ( "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + "github.com/hatchet-dev/hatchet/pkg/analytics" "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" transformers "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" @@ -31,6 +33,14 @@ func (t *TasksService) V1TaskGet(ctx echo.Context, request gen.V1TaskGetRequestO return nil, echo.NewHTTPError(500, "Task type assertion failed") } + tenant := ctx.Get("tenant").(*sqlcv1.Tenant) + + if ts := task.InsertedAt; ts.Valid && v1handlers.IsBeforeRetention(ts.Time, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx.Request().Context(), analytics.TaskRun, analytics.Get, analytics.Properties{ + "outside_retention": true, + }) + } + attempt := request.Params.Attempt var retryCount *int diff --git a/api/v1/server/handlers/v1/workflow-runs/get.go b/api/v1/server/handlers/v1/workflow-runs/get.go index 5a0d731c4a..781b83e150 100644 --- a/api/v1/server/handlers/v1/workflow-runs/get.go +++ b/api/v1/server/handlers/v1/workflow-runs/get.go @@ -8,7 +8,9 @@ import ( "github.com/jackc/pgx/v5" "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + "github.com/hatchet-dev/hatchet/pkg/analytics" v1 "github.com/hatchet-dev/hatchet/pkg/repository" "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" @@ -20,6 +22,12 @@ func (t *V1WorkflowRunsService) V1WorkflowRunGet(ctx echo.Context, request gen.V tenantId := tenant.ID rawWorkflowRun := ctx.Get("v1-workflow-run").(*v1.V1WorkflowRunPopulator) + if ts := rawWorkflowRun.WorkflowRun.CreatedAt; ts.Valid && v1handlers.IsBeforeRetention(ts.Time, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx.Request().Context(), analytics.WorkflowRun, analytics.Get, analytics.Properties{ + "outside_retention": true, + }) + } + requestContext := ctx.Request().Context() details, err := t.getWorkflowRunDetails( diff --git a/api/v1/server/handlers/v1/workflow-runs/list.go b/api/v1/server/handlers/v1/workflow-runs/list.go index b283848397..a81b152aa7 100644 --- a/api/v1/server/handlers/v1/workflow-runs/list.go +++ b/api/v1/server/handlers/v1/workflow-runs/list.go @@ -7,12 +7,13 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" + v1handlers "github.com/hatchet-dev/hatchet/api/v1/server/handlers/v1" "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + transformers "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" + "github.com/hatchet-dev/hatchet/pkg/analytics" v1 "github.com/hatchet-dev/hatchet/pkg/repository" "github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1" "github.com/hatchet-dev/hatchet/pkg/telemetry" - - transformers "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers/v1" ) func allOlapStatuses(runningFilter *gen.V1RunningFilter) []sqlcv1.V1ReadableStatusOlap { @@ -97,13 +98,21 @@ func normalizeWorkflowRunStatuses(statuses []gen.V1TaskStatus, runningFilter *ge return normalized } -func (t *V1WorkflowRunsService) WithDags(ctx context.Context, request gen.V1WorkflowRunListRequestObject, tenantId uuid.UUID) (gen.V1WorkflowRunListResponseObject, error) { +func (t *V1WorkflowRunsService) WithDags(ctx context.Context, request gen.V1WorkflowRunListRequestObject, tenant *sqlcv1.Tenant) (gen.V1WorkflowRunListResponseObject, error) { ctx, span := telemetry.NewSpan(ctx, "v1-workflow-runs-list-with-dags-tasks") defer span.End() + tenantId := tenant.ID + since := request.Params.Since + + if v1handlers.IsBeforeRetention(since, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx, analytics.WorkflowRun, analytics.List, analytics.Properties{ + "outside_retention": true, + }) + } + var ( statuses = allOlapStatuses(request.Params.RunningFilter) - since = request.Params.Since limit int64 = 50 offset int64 ) @@ -244,13 +253,21 @@ func (t *V1WorkflowRunsService) WithDags(ctx context.Context, request gen.V1Work ), nil } -func (t *V1WorkflowRunsService) OnlyTasks(ctx context.Context, request gen.V1WorkflowRunListRequestObject, tenantId uuid.UUID) (gen.V1WorkflowRunListResponseObject, error) { +func (t *V1WorkflowRunsService) OnlyTasks(ctx context.Context, request gen.V1WorkflowRunListRequestObject, tenant *sqlcv1.Tenant) (gen.V1WorkflowRunListResponseObject, error) { ctx, span := telemetry.NewSpan(ctx, "v1-workflow-runs-list-only-tasks") defer span.End() + tenantId := tenant.ID + since := request.Params.Since + + if v1handlers.IsBeforeRetention(since, tenant.DataRetentionPeriod) { + t.config.Analytics.Count(ctx, analytics.WorkflowRun, analytics.List, analytics.Properties{ + "outside_retention": true, + }) + } + var ( statuses = allOlapStatuses(request.Params.RunningFilter) - since = request.Params.Since workflowIds = []uuid.UUID{} limit int64 = 50 offset int64 @@ -353,15 +370,14 @@ func (t *V1WorkflowRunsService) OnlyTasks(ctx context.Context, request gen.V1Wor func (t *V1WorkflowRunsService) V1WorkflowRunList(ctx echo.Context, request gen.V1WorkflowRunListRequestObject) (gen.V1WorkflowRunListResponseObject, error) { tenant := ctx.Get("tenant").(*sqlcv1.Tenant) - tenantId := tenant.ID spanContext, span := telemetry.NewSpan(ctx.Request().Context(), "v1-workflow-runs-list") defer span.End() if request.Params.OnlyTasks { - return t.OnlyTasks(spanContext, request, tenantId) + return t.OnlyTasks(spanContext, request, tenant) } else { - return t.WithDags(spanContext, request, tenantId) + return t.WithDags(spanContext, request, tenant) } } diff --git a/api/v1/server/oas/gen/openapi.gen.go b/api/v1/server/oas/gen/openapi.gen.go index 56e5636d30..2ccf52bbe6 100644 --- a/api/v1/server/oas/gen/openapi.gen.go +++ b/api/v1/server/oas/gen/openapi.gen.go @@ -1151,9 +1151,12 @@ type Tenant struct { AlertMemberEmails *bool `json:"alertMemberEmails,omitempty"` // AnalyticsOptOut Whether the tenant has opted out of analytics. - AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"` - Environment *TenantEnvironment `json:"environment,omitempty"` - Metadata APIResourceMeta `json:"metadata"` + AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"` + + // DataRetentionPeriod The data retention period for the tenant, e.g. 720h. + DataRetentionPeriod *string `json:"dataRetentionPeriod,omitempty"` + Environment *TenantEnvironment `json:"environment,omitempty"` + Metadata APIResourceMeta `json:"metadata"` // Name The name of the tenant. Name string `json:"name"` @@ -16993,269 +16996,269 @@ var swaggerSpec = []string{ "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", - "/n8BAAD//0mZWRb9YwMA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1/server/oas/transformers/tenant.go b/api/v1/server/oas/transformers/tenant.go index ff7f622e46..44ae347025 100644 --- a/api/v1/server/oas/transformers/tenant.go +++ b/api/v1/server/oas/transformers/tenant.go @@ -17,13 +17,14 @@ func ToTenant(tenant *sqlcv1.Tenant) *gen.Tenant { } return &gen.Tenant{ - Metadata: *toAPIMetadata(tenant.ID, tenant.CreatedAt.Time, tenant.UpdatedAt.Time), - Name: tenant.Name, - Slug: tenant.Slug, - AnalyticsOptOut: &tenant.AnalyticsOptOut, - AlertMemberEmails: &tenant.AlertMemberEmails, - Version: gen.TenantVersion(tenant.Version), - Environment: environment, + Metadata: *toAPIMetadata(tenant.ID, tenant.CreatedAt.Time, tenant.UpdatedAt.Time), + Name: tenant.Name, + Slug: tenant.Slug, + AnalyticsOptOut: &tenant.AnalyticsOptOut, + AlertMemberEmails: &tenant.AlertMemberEmails, + Version: gen.TenantVersion(tenant.Version), + Environment: environment, + DataRetentionPeriod: &tenant.DataRetentionPeriod, } } diff --git a/api/v1/server/oas/transformers/user.go b/api/v1/server/oas/transformers/user.go index 95526a22ff..2dcc62a599 100644 --- a/api/v1/server/oas/transformers/user.go +++ b/api/v1/server/oas/transformers/user.go @@ -39,13 +39,14 @@ func ToTenantMember(tenantMember *sqlcv1.PopulateTenantMembersRow) *gen.TenantMe Name: v1.StringPtr(tenantMember.Name.String), }, Tenant: &gen.Tenant{ - Metadata: *toAPIMetadata(tenantMember.TenantId, tenantMember.TenantCreatedAt.Time, tenantMember.TenantUpdatedAt.Time), - Name: tenantMember.TenantName, - Slug: tenantMember.TenantSlug, - AnalyticsOptOut: &tenantMember.AnalyticsOptOut, - AlertMemberEmails: &tenantMember.AlertMemberEmails, - Version: gen.TenantVersion(tenantMember.TenantVersion), - Environment: environment, + Metadata: *toAPIMetadata(tenantMember.TenantId, tenantMember.TenantCreatedAt.Time, tenantMember.TenantUpdatedAt.Time), + Name: tenantMember.TenantName, + Slug: tenantMember.TenantSlug, + AnalyticsOptOut: &tenantMember.AnalyticsOptOut, + AlertMemberEmails: &tenantMember.AlertMemberEmails, + Version: gen.TenantVersion(tenantMember.TenantVersion), + Environment: environment, + DataRetentionPeriod: &tenantMember.TenantDataRetentionPeriod, }, Role: gen.TenantMemberRole(tenantMember.Role), } diff --git a/frontend/app/src/components/v1/molecules/data-table/data-table-options.tsx b/frontend/app/src/components/v1/molecules/data-table/data-table-options.tsx index acdd6079b5..e3f7ff9c3b 100644 --- a/frontend/app/src/components/v1/molecules/data-table/data-table-options.tsx +++ b/frontend/app/src/components/v1/molecules/data-table/data-table-options.tsx @@ -142,7 +142,10 @@ function FilterControl({ 1 hour 6 hours 1 day + 3 days 7 days + 14 days + 30 days Custom diff --git a/frontend/app/src/components/v1/retention-banner.tsx b/frontend/app/src/components/v1/retention-banner.tsx new file mode 100644 index 0000000000..8e1a6eda1b --- /dev/null +++ b/frontend/app/src/components/v1/retention-banner.tsx @@ -0,0 +1,74 @@ +import { DocsButton } from '@/components/v1/docs/docs-button'; +import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert'; +import { Button } from '@/components/v1/ui/button'; +import useCloud from '@/hooks/use-cloud'; +import { docsPages } from '@/lib/generated/docs'; +import { formatRetentionPeriod } from '@/lib/utils/retention'; +import { appRoutes } from '@/router'; +import { Link, useParams } from '@tanstack/react-router'; +import { Clock } from 'lucide-react'; + +interface RetentionBannerProps { + retentionPeriod: string; +} + +export function RetentionBanner({ retentionPeriod }: RetentionBannerProps) { + const { isCloudEnabled } = useCloud(); + const label = formatRetentionPeriod(retentionPeriod); + + if (isCloudEnabled) { + return ; + } + + return ; +} + +function CloudRetentionBanner({ label }: { label: string }) { + const { tenant: tenantId } = useParams({ from: appRoutes.tenantRoute.to }); + + return ( + + + Data outside retention window + + + Your current plan retains data for {label}. Data outside this window + is no longer available. Upgrade your plan to extend your retention + period. + +
+ + + +
+
+
+ ); +} + +function OSSRetentionBanner({ label }: { label: string }) { + return ( + + + Data outside retention window + + + Your instance retains data for {label}. Data outside this window has + been pruned and is no longer available. You can adjust the retention + period in your server configuration. + +
+ +
+
+
+ ); +} diff --git a/frontend/app/src/lib/api/generated/data-contracts.ts b/frontend/app/src/lib/api/generated/data-contracts.ts index addd9d5a53..4a7c947c79 100644 --- a/frontend/app/src/lib/api/generated/data-contracts.ts +++ b/frontend/app/src/lib/api/generated/data-contracts.ts @@ -900,6 +900,8 @@ export interface Tenant { version: TenantVersion; /** The environment type of the tenant. */ environment?: TenantEnvironment; + /** The data retention period for the tenant, e.g. 720h. */ + dataRetentionPeriod?: string; } export interface V1EventWorkflowRunSummary { diff --git a/frontend/app/src/lib/utils/retention.ts b/frontend/app/src/lib/utils/retention.ts new file mode 100644 index 0000000000..dd6806e75c --- /dev/null +++ b/frontend/app/src/lib/utils/retention.ts @@ -0,0 +1,72 @@ +/** + * Parse a Go-style duration string (e.g. "720h", "168h0m0s") into milliseconds. + * Supports hours (h), minutes (m), and seconds (s). + */ +export function parseGoDuration(period: string): number | null { + const re = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/; + const match = period.trim().match(re); + + if (!match || (!match[1] && !match[2] && !match[3])) { + return null; + } + + const hours = parseInt(match[1] || '0', 10); + const minutes = parseInt(match[2] || '0', 10); + const seconds = parseInt(match[3] || '0', 10); + + return (hours * 3600 + minutes * 60 + seconds) * 1000; +} + +/** + * Returns the retention boundary Date (now - retentionPeriod). + */ +export function getRetentionBoundary(period: string): Date | null { + const ms = parseGoDuration(period); + if (ms === null) { + return null; + } + return new Date(Date.now() - ms); +} + +/** + * Returns true if the given date falls before the retention boundary. + * Uses a 5-minute tolerance so that filter windows matching the retention + * period (e.g. "1 day" filter with 24h retention) don't falsely trigger. + */ +const RETENTION_TOLERANCE_MS = 5 * 60 * 1000; + +export function isBeforeRetention( + date: string | Date, + period: string, +): boolean { + const boundary = getRetentionBoundary(period); + if (!boundary) { + return false; + } + const d = typeof date === 'string' ? new Date(date) : date; + return d.getTime() < boundary.getTime() - RETENTION_TOLERANCE_MS; +} + +/** + * Formats a Go-style duration string into a human-readable label. + * E.g. "720h" -> "30 days", "168h0m0s" -> "7 days", "24h" -> "1 day". + */ +export function formatRetentionPeriod(period: string): string { + const ms = parseGoDuration(period); + if (ms === null) { + return period; + } + + const hours = ms / (1000 * 3600); + + if (hours >= 24 && hours % 24 === 0) { + const days = hours / 24; + return days === 1 ? '1 day' : `${days} days`; + } + + if (hours >= 1 && hours === Math.floor(hours)) { + return hours === 1 ? '1 hour' : `${hours} hours`; + } + + return period; +} diff --git a/frontend/app/src/pages/main/v1/events/index.tsx b/frontend/app/src/pages/main/v1/events/index.tsx index d53b18ca90..846dc5f924 100644 --- a/frontend/app/src/pages/main/v1/events/index.tsx +++ b/frontend/app/src/pages/main/v1/events/index.tsx @@ -17,17 +17,21 @@ import { DataTable } from '@/components/v1/molecules/data-table/data-table'; import { ToolbarType } from '@/components/v1/molecules/data-table/data-table-toolbar'; import RelativeDate from '@/components/v1/molecules/relative-date'; import { SimpleTable } from '@/components/v1/molecules/simple-table/simple-table'; +import { RetentionBanner } from '@/components/v1/retention-banner'; import { Button } from '@/components/v1/ui/button'; import { CodeHighlighter } from '@/components/v1/ui/code-highlighter'; import { Separator } from '@/components/v1/ui/separator'; import { useSidePanel } from '@/hooks/use-side-panel'; import { V1Event, V1Filter } from '@/lib/api'; import { docsPages } from '@/lib/generated/docs'; +import { isBeforeRetention } from '@/lib/utils/retention'; +import { useAppContext } from '@/providers/app-context'; import { VisibilityState } from '@tanstack/react-table'; import { CheckIcon } from 'lucide-react'; import { useMemo, useState } from 'react'; export default function Events() { + const { tenant } = useAppContext(); const [openMetadataPopover, setOpenMetadataPopover] = useState( null, ); @@ -77,13 +81,19 @@ export default function Events() { setOpenPayloadPopover, }); + const defaultSince = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + + const isOutsideRetention = + !!tenant?.dataRetentionPeriod && + isBeforeRetention(defaultSince, tenant.dataRetentionPeriod); + return ( <> -

No events found

-
- + isOutsideRetention ? ( +
+
+ +
-
+ ) : ( +
+

No events found

+
+ +
+
+ ) } /> diff --git a/frontend/app/src/pages/main/v1/filters/index.tsx b/frontend/app/src/pages/main/v1/filters/index.tsx index 8b78ac3704..bc453de646 100644 --- a/frontend/app/src/pages/main/v1/filters/index.tsx +++ b/frontend/app/src/pages/main/v1/filters/index.tsx @@ -97,7 +97,7 @@ export default function Filters() {

No filters found

diff --git a/frontend/app/src/pages/main/v1/logs/index.tsx b/frontend/app/src/pages/main/v1/logs/index.tsx index 90ba02d64c..650dab3c21 100644 --- a/frontend/app/src/pages/main/v1/logs/index.tsx +++ b/frontend/app/src/pages/main/v1/logs/index.tsx @@ -11,6 +11,7 @@ import type { AutocompleteSuggestion } from '@/components/v1/cloud/logging/log-s import { LogViewer } from '@/components/v1/cloud/logging/log-viewer'; import { SearchBarWithFilters } from '@/components/v1/molecules/search-bar-with-filters/search-bar-with-filters'; import { DateTimePicker } from '@/components/v1/molecules/time-picker/date-time-picker'; +import { RetentionBanner } from '@/components/v1/retention-banner'; import { Button } from '@/components/v1/ui/button'; import { Select, @@ -19,12 +20,16 @@ import { SelectTrigger, SelectValue, } from '@/components/v1/ui/select'; +import { Skeleton } from '@/components/v1/ui/skeleton'; import { FeatureFlagId, useIsFeatureEnabled } from '@/hooks/use-feature-flags'; import { useSidePanel } from '@/hooks/use-side-panel'; +import { isBeforeRetention } from '@/lib/utils/retention'; +import { useAppContext } from '@/providers/app-context'; import { XCircleIcon } from 'lucide-react'; import { useCallback, useMemo } from 'react'; export default function TenantLogsPage() { + const { tenant } = useAppContext(); const { isEnabled: isWorkflowFilterEnabled } = useIsFeatureEnabled( FeatureFlagId.TenantLogWorkflowFilterEnabled, true, @@ -169,16 +174,39 @@ export default function TenantLogsPage() { onRefetch={refetch} />
- + {tenant?.dataRetentionPeriod && + isBeforeRetention(chartSince, tenant.dataRetentionPeriod) ? ( +
+
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ + + +
+ ))} +
+
+
+ +
+
+
+ ) : ( + + )} ); } diff --git a/frontend/app/src/pages/main/v1/workflow-runs-v1/$run/index.tsx b/frontend/app/src/pages/main/v1/workflow-runs-v1/$run/index.tsx index 17773e2a37..51e35847f2 100644 --- a/frontend/app/src/pages/main/v1/workflow-runs-v1/$run/index.tsx +++ b/frontend/app/src/pages/main/v1/workflow-runs-v1/$run/index.tsx @@ -21,10 +21,12 @@ import { WorkflowRunInputDialog } from './v2components/workflow-run-input'; import { WorkflowRunLogs } from './v2components/workflow-run-logs'; import WorkflowRunVisualizer from './v2components/workflow-run-visualizer-v2'; import type { TaskSummaryForSynthesis } from '@/components/v1/agent-prism/convert-otel-spans-to-agent-prism-span-tree'; +import { RetentionBanner } from '@/components/v1/retention-banner'; import { Badge } from '@/components/v1/ui/badge'; import { CodeHighlighter } from '@/components/v1/ui/code-highlighter'; import { Spinner } from '@/components/v1/ui/loading'; import { Separator } from '@/components/v1/ui/separator'; +import { Skeleton } from '@/components/v1/ui/skeleton'; import { Tabs, TabsContent, @@ -40,7 +42,9 @@ import api, { } from '@/lib/api'; import { preferredWorkflowRunViewAtom } from '@/lib/atoms'; import { getErrorStatus, shouldRetryQueryError } from '@/lib/error-utils'; +import { isBeforeRetention } from '@/lib/utils/retention'; import { ResourceNotFound } from '@/pages/error/components/resource-not-found'; +import { useAppContext } from '@/providers/app-context'; import { appRoutes } from '@/router'; import { useQuery } from '@tanstack/react-query'; import { useParams } from '@tanstack/react-router'; @@ -57,6 +61,51 @@ class StatusError extends Error { } } +function RetentionExpired({ retentionPeriod }: { retentionPeriod: string }) { + return ( +
+
+
+ + + +
+ + +
+
+
+ + + +
+
+ +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + +
+ ))} +
+
+
+
+
+ +
+
+
+ ); +} + function statusToBadgeVariant(status: V1TaskStatus) { switch (status) { case V1TaskStatus.COMPLETED: @@ -137,6 +186,7 @@ async function fetchDAGRun(id: string) { export default function Run() { const params = useParams({ from: appRoutes.tenantRunRoute.to }); const { run } = params; + const { tenant } = useAppContext(); const taskRunQuery = useQuery({ queryKey: ['workflow-run', run], @@ -176,6 +226,10 @@ export default function Run() { throw new Error(`Task or Workflow Run with ID ${run} not found`); }, refetchInterval: (query) => { + if (query.state.error) { + return false; + } + const status = query.state.data?.status; if (isTerminalState(status)) { @@ -194,7 +248,6 @@ export default function Run() { if (taskRunQuery.isError) { const status = getErrorStatus(taskRunQuery.error); - // Treat malformed IDs (often 400) and missing resources (404) as not found. if (status === 400 || status === 404) { return ( ; + } + if (runData.type === 'task') { return ( diff --git a/frontend/app/src/pages/main/v1/workflow-runs-v1/components/runs-table.tsx b/frontend/app/src/pages/main/v1/workflow-runs-v1/components/runs-table.tsx index 2f19231cfa..8153650d47 100644 --- a/frontend/app/src/pages/main/v1/workflow-runs-v1/components/runs-table.tsx +++ b/frontend/app/src/pages/main/v1/workflow-runs-v1/components/runs-table.tsx @@ -10,6 +10,7 @@ import { ZoomableChart, } from '@/components/v1/molecules/charts/zoomable'; import { DataTable } from '@/components/v1/molecules/data-table/data-table.tsx'; +import { RetentionBanner } from '@/components/v1/retention-banner'; import { CodeHighlighter } from '@/components/v1/ui/code-highlighter'; import { Dialog, @@ -23,14 +24,17 @@ import { Skeleton } from '@/components/v1/ui/skeleton'; import { Toaster } from '@/components/v1/ui/toaster'; import { useRefetchInterval } from '@/contexts/refetch-interval-context'; import { useSidePanel } from '@/hooks/use-side-panel'; -import { useCurrentTenantId } from '@/hooks/use-tenant'; import { queries } from '@/lib/api'; import { docsPages } from '@/lib/generated/docs'; +import { isBeforeRetention } from '@/lib/utils/retention'; +import { useAppContext } from '@/providers/app-context'; +import { appRoutes } from '@/router'; import { useQuery } from '@tanstack/react-query'; +import { useParams } from '@tanstack/react-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; const GetWorkflowChart = () => { - const { tenantId } = useCurrentTenantId(); + const { tenant: tenantId } = useParams({ from: appRoutes.tenantRoute.to }); const { refetchInterval } = useRefetchInterval(); const { @@ -84,7 +88,7 @@ const GetWorkflowChart = () => { }; export function RunsTable({ leftLabel }: { leftLabel?: string }) { - const { tenantId } = useCurrentTenantId(); + const { tenant, tenantId } = useAppContext(); const sidePanel = useSidePanel(); const { setIsFrozen } = useRefetchInterval(); @@ -160,7 +164,7 @@ export function RunsTable({ leftLabel }: { leftLabel?: string }) { const tableColumns = useMemo( () => columns( - tenantId, + tenantId!, selectedAdditionalMetaRunId, handleAdditionalMetadataClick, handleTaskRunIdClick, @@ -194,6 +198,11 @@ export function RunsTable({ leftLabel }: { leftLabel?: string }) { const isRunningFirstLoad = isRunsLoading || isStatusCountsLoading; + const isOutsideRetention = + !!tenant?.dataRetentionPeriod && + !!filters.apiFilters.since && + isBeforeRetention(filters.apiFilters.since, tenant.dataRetentionPeriod); + const leftActions = [ ...(!hideCounts ? [ @@ -253,21 +262,31 @@ export function RunsTable({ leftLabel }: { leftLabel?: string }) {
-

No runs found

-
- + isOutsideRetention ? ( +
+
+ +
+
+ ) : ( +
+

No runs found

+
+ +
-
+ ) } isLoading={isRunningFirstLoad} columns={tableColumns} columnVisibility={columnVisibility} setColumnVisibility={setColumnVisibility} - data={tableRows} + data={isOutsideRetention ? [] : tableRows} filters={toolbarFilters} leftActions={leftActions} columnFilters={filters.columnFilters} diff --git a/frontend/app/src/pages/main/v1/workflow-runs-v1/hooks/use-runs-table-filters.tsx b/frontend/app/src/pages/main/v1/workflow-runs-v1/hooks/use-runs-table-filters.tsx index 76803600c4..bec98a876d 100644 --- a/frontend/app/src/pages/main/v1/workflow-runs-v1/hooks/use-runs-table-filters.tsx +++ b/frontend/app/src/pages/main/v1/workflow-runs-v1/hooks/use-runs-table-filters.tsx @@ -16,25 +16,21 @@ import { ColumnFiltersState } from '@tanstack/react-table'; import { useCallback, useMemo } from 'react'; import { z } from 'zod'; -type TimeWindow = '1h' | '6h' | '1d' | '7d'; - -const getCreatedAfterFromTimeRange = (timeWindow: TimeWindow): string => { - switch (timeWindow) { - case '1h': - return new Date(Date.now() - 60 * 60 * 1000).toISOString(); - case '6h': - return new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(); - case '1d': - return new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - case '7d': - return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); - default: { - const exhaustiveCheck: never = timeWindow; - throw new Error(`Unhandled time range: ${exhaustiveCheck}`); - } - } +type TimeWindow = '1h' | '6h' | '1d' | '3d' | '7d' | '14d' | '30d'; + +const TIME_WINDOW_MS: Record = { + '1h': 60 * 60 * 1000, + '6h': 6 * 60 * 60 * 1000, + '1d': 24 * 60 * 60 * 1000, + '3d': 3 * 24 * 60 * 60 * 1000, + '7d': 7 * 24 * 60 * 60 * 1000, + '14d': 14 * 24 * 60 * 60 * 1000, + '30d': 30 * 24 * 60 * 60 * 1000, }; +const getCreatedAfterFromTimeRange = (timeWindow: TimeWindow): string => + new Date(Date.now() - TIME_WINDOW_MS[timeWindow]).toISOString(); + export type AdditionalMetadataProp = { key: string; value: string; @@ -65,7 +61,7 @@ export type FilterActions = { const createApiFilterSchema = (initialValues?: { workflowIds?: string[] }) => z.object({ - tw: z.enum(['1h', '6h', '1d', '7d']).default('1d'), // time window preset + tw: z.enum(['1h', '6h', '1d', '3d', '7d', '14d', '30d']).default('1d'), // time window preset ctr: z.boolean().default(false), // whether using custom range s: z.string().optional(), // since u: z.string().optional(), // until diff --git a/pkg/client/rest/gen.go b/pkg/client/rest/gen.go index 6f717a5261..1b9f0f73d8 100644 --- a/pkg/client/rest/gen.go +++ b/pkg/client/rest/gen.go @@ -1148,9 +1148,12 @@ type Tenant struct { AlertMemberEmails *bool `json:"alertMemberEmails,omitempty"` // AnalyticsOptOut Whether the tenant has opted out of analytics. - AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"` - Environment *TenantEnvironment `json:"environment,omitempty"` - Metadata APIResourceMeta `json:"metadata"` + AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"` + + // DataRetentionPeriod The data retention period for the tenant, e.g. 720h. + DataRetentionPeriod *string `json:"dataRetentionPeriod,omitempty"` + Environment *TenantEnvironment `json:"environment,omitempty"` + Metadata APIResourceMeta `json:"metadata"` // Name The name of the tenant. Name string `json:"name"` diff --git a/pkg/repository/sqlcv1/tenants.sql b/pkg/repository/sqlcv1/tenants.sql index 685e387456..9acdbec576 100644 --- a/pkg/repository/sqlcv1/tenants.sql +++ b/pkg/repository/sqlcv1/tenants.sql @@ -35,7 +35,8 @@ SET "name" = COALESCE(sqlc.narg('name')::text, "name"), "analyticsOptOut" = COALESCE(sqlc.narg('analyticsOptOut')::boolean, "analyticsOptOut"), "alertMemberEmails" = COALESCE(sqlc.narg('alertMemberEmails')::boolean, "alertMemberEmails"), - "version" = COALESCE(sqlc.narg('version')::"TenantMajorEngineVersion", "version") + "version" = COALESCE(sqlc.narg('version')::"TenantMajorEngineVersion", "version"), + "dataRetentionPeriod" = COALESCE(sqlc.narg('dataRetentionPeriod')::text, "dataRetentionPeriod") WHERE "id" = sqlc.arg('id')::uuid RETURNING *; @@ -611,7 +612,8 @@ SELECT t."alertMemberEmails" as "alertMemberEmails", t."analyticsOptOut" as "analyticsOptOut", t."version" as "tenantVersion", - t."environment" as "tenantEnvironment" + t."environment" as "tenantEnvironment", + t."dataRetentionPeriod" as "tenantDataRetentionPeriod" FROM "TenantMember" tm JOIN diff --git a/pkg/repository/sqlcv1/tenants.sql.go b/pkg/repository/sqlcv1/tenants.sql.go index a44235f00d..8936ccc430 100644 --- a/pkg/repository/sqlcv1/tenants.sql.go +++ b/pkg/repository/sqlcv1/tenants.sql.go @@ -1168,7 +1168,8 @@ SELECT t."alertMemberEmails" as "alertMemberEmails", t."analyticsOptOut" as "analyticsOptOut", t."version" as "tenantVersion", - t."environment" as "tenantEnvironment" + t."environment" as "tenantEnvironment", + t."dataRetentionPeriod" as "tenantDataRetentionPeriod" FROM "TenantMember" tm JOIN @@ -1180,23 +1181,24 @@ WHERE ` type PopulateTenantMembersRow struct { - ID uuid.UUID `json:"id"` - CreatedAt pgtype.Timestamp `json:"createdAt"` - UpdatedAt pgtype.Timestamp `json:"updatedAt"` - TenantId uuid.UUID `json:"tenantId"` - UserId uuid.UUID `json:"userId"` - Role TenantMemberRole `json:"role"` - Email string `json:"email"` - Name pgtype.Text `json:"name"` - TenantId_2 uuid.UUID `json:"tenantId_2"` - TenantCreatedAt pgtype.Timestamp `json:"tenantCreatedAt"` - TenantUpdatedAt pgtype.Timestamp `json:"tenantUpdatedAt"` - TenantName string `json:"tenantName"` - TenantSlug string `json:"tenantSlug"` - AlertMemberEmails bool `json:"alertMemberEmails"` - AnalyticsOptOut bool `json:"analyticsOptOut"` - TenantVersion TenantMajorEngineVersion `json:"tenantVersion"` - TenantEnvironment NullTenantEnvironment `json:"tenantEnvironment"` + ID uuid.UUID `json:"id"` + CreatedAt pgtype.Timestamp `json:"createdAt"` + UpdatedAt pgtype.Timestamp `json:"updatedAt"` + TenantId uuid.UUID `json:"tenantId"` + UserId uuid.UUID `json:"userId"` + Role TenantMemberRole `json:"role"` + Email string `json:"email"` + Name pgtype.Text `json:"name"` + TenantId_2 uuid.UUID `json:"tenantId_2"` + TenantCreatedAt pgtype.Timestamp `json:"tenantCreatedAt"` + TenantUpdatedAt pgtype.Timestamp `json:"tenantUpdatedAt"` + TenantName string `json:"tenantName"` + TenantSlug string `json:"tenantSlug"` + AlertMemberEmails bool `json:"alertMemberEmails"` + AnalyticsOptOut bool `json:"analyticsOptOut"` + TenantVersion TenantMajorEngineVersion `json:"tenantVersion"` + TenantEnvironment NullTenantEnvironment `json:"tenantEnvironment"` + TenantDataRetentionPeriod string `json:"tenantDataRetentionPeriod"` } func (q *Queries) PopulateTenantMembers(ctx context.Context, db DBTX, ids []uuid.UUID) ([]*PopulateTenantMembersRow, error) { @@ -1226,6 +1228,7 @@ func (q *Queries) PopulateTenantMembers(ctx context.Context, db DBTX, ids []uuid &i.AnalyticsOptOut, &i.TenantVersion, &i.TenantEnvironment, + &i.TenantDataRetentionPeriod, ); err != nil { return nil, err } @@ -1512,18 +1515,20 @@ SET "name" = COALESCE($1::text, "name"), "analyticsOptOut" = COALESCE($2::boolean, "analyticsOptOut"), "alertMemberEmails" = COALESCE($3::boolean, "alertMemberEmails"), - "version" = COALESCE($4::"TenantMajorEngineVersion", "version") + "version" = COALESCE($4::"TenantMajorEngineVersion", "version"), + "dataRetentionPeriod" = COALESCE($5::text, "dataRetentionPeriod") WHERE - "id" = $5::uuid + "id" = $6::uuid RETURNING id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment ` type UpdateTenantParams struct { - Name pgtype.Text `json:"name"` - AnalyticsOptOut pgtype.Bool `json:"analyticsOptOut"` - AlertMemberEmails pgtype.Bool `json:"alertMemberEmails"` - Version NullTenantMajorEngineVersion `json:"version"` - ID uuid.UUID `json:"id"` + Name pgtype.Text `json:"name"` + AnalyticsOptOut pgtype.Bool `json:"analyticsOptOut"` + AlertMemberEmails pgtype.Bool `json:"alertMemberEmails"` + Version NullTenantMajorEngineVersion `json:"version"` + DataRetentionPeriod pgtype.Text `json:"dataRetentionPeriod"` + ID uuid.UUID `json:"id"` } func (q *Queries) UpdateTenant(ctx context.Context, db DBTX, arg UpdateTenantParams) (*Tenant, error) { @@ -1532,6 +1537,7 @@ func (q *Queries) UpdateTenant(ctx context.Context, db DBTX, arg UpdateTenantPar arg.AnalyticsOptOut, arg.AlertMemberEmails, arg.Version, + arg.DataRetentionPeriod, arg.ID, ) var i Tenant diff --git a/pkg/repository/tenant.go b/pkg/repository/tenant.go index 801c6395b1..73745d67dd 100644 --- a/pkg/repository/tenant.go +++ b/pkg/repository/tenant.go @@ -48,6 +48,8 @@ type UpdateTenantOpts struct { AlertMemberEmails *bool `validate:"omitempty"` Version *sqlcv1.NullTenantMajorEngineVersion `validate:"omitempty"` + + DataRetentionPeriod *string `validate:"omitempty"` } type CreateTenantMemberOpts struct { @@ -362,6 +364,10 @@ func (r *tenantRepository) UpdateTenant(ctx context.Context, id uuid.UUID, opts params.Version = *opts.Version } + if opts.DataRetentionPeriod != nil { + params.DataRetentionPeriod = sqlchelpers.TextFromStr(*opts.DataRetentionPeriod) + } + updated, err := r.queries.UpdateTenant( ctx, r.pool,