packages feed

kansoku-api 0.2.0.0 → 0.3.0.0

raw patch · 11 files changed

+296/−14 lines, 11 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Kansoku.Analysis.Api: Analyses :: (mode :- ("analyses" :> (MetricsAuth :> (Description "Save opaque client state without executing or interpreting it. Names are trimmed, 1..120 Unicode characters; descriptions retain whitespace, maximum 500 characters. Metadata rejects zero characters. documentVersion is a positive integer; document must be an object, maximum 32768 compact Aeson UTF-8 bytes. Raw analysis request bodies have a separate 5 MiB transport limit, independent of the ingest body setting. Duplicate names are allowed. The positive KANSOKU_ANALYSES_MAX_PER_ACCOUNT limit defaults to 1000 and must match on all replicas. A full account returns non-retryable 409 analysis_limit_reached." :> (ReqBody '[JSON] AnalysisInput :> AnalysisReply 'POST SavedAnalysis))))) -> (mode :- AtAnalysis (AnalysisReply 'GET SavedAnalysis)) -> (mode :- ("analyses" :> (MetricsAuth :> (Description "Current-read pages ordered by updatedAt descending, id ascending. Default 20, maximum 100. Both directions keep canonical order. Cursors preserve microseconds, survive deletion/replacement of their source row, and must be reused with the same kind filter. No cross-request snapshot; refresh from the first page after edits. Invalid kind returns a 400 problem document; invalid pagination uses the existing Relay JSON error." :> (QueryParam "kind" AnalysisKind :> (RelayPage 20 100 :> PageReply SavedAnalysis)))))) -> (mode :- AtAnalysis (Description "Full replacement with required positive revision from the last read. Stale revision returns 409 analysis_revision_conflict. Every successful replacement increments revision, preserves createdAt and advances updatedAt, including identical content. The same field and byte limits as create apply." :> (ReqBody '[JSON] AnalysisReplace :> AnalysisReply 'PUT SavedAnalysis))) -> (mode :- AtAnalysis (Description "Delete with the required positive revision from the last read. Stale revision returns 409; missing or foreign IDs return 404, including repeated deletion." :> (QueryParam' '[Required, Strict] "revision" Revision :> AnalysisReply 'DELETE Bool))) -> Analyses (mode :: k)
+ Kansoku.Analysis.Api: AnalysisBadRequest :: ProblemDetails -> AnalysisResult a
+ Kansoku.Analysis.Api: AnalysisConflict :: ProblemDetails -> AnalysisResult a
+ Kansoku.Analysis.Api: AnalysisNotFound :: ProblemDetails -> AnalysisResult a
+ Kansoku.Analysis.Api: AnalysisOk :: a -> AnalysisResult a
+ Kansoku.Analysis.Api: AnalysisUnavailable :: ProblemDetails -> AnalysisResult a
+ Kansoku.Analysis.Api: [createAnalysis] :: Analyses (mode :: k) -> mode :- ("analyses" :> (MetricsAuth :> (Description "Save opaque client state without executing or interpreting it. Names are trimmed, 1..120 Unicode characters; descriptions retain whitespace, maximum 500 characters. Metadata rejects zero characters. documentVersion is a positive integer; document must be an object, maximum 32768 compact Aeson UTF-8 bytes. Raw analysis request bodies have a separate 5 MiB transport limit, independent of the ingest body setting. Duplicate names are allowed. The positive KANSOKU_ANALYSES_MAX_PER_ACCOUNT limit defaults to 1000 and must match on all replicas. A full account returns non-retryable 409 analysis_limit_reached." :> (ReqBody '[JSON] AnalysisInput :> AnalysisReply 'POST SavedAnalysis))))
+ Kansoku.Analysis.Api: [deleteAnalysis] :: Analyses (mode :: k) -> mode :- AtAnalysis (Description "Delete with the required positive revision from the last read. Stale revision returns 409; missing or foreign IDs return 404, including repeated deletion." :> (QueryParam' '[Required, Strict] "revision" Revision :> AnalysisReply 'DELETE Bool))
+ Kansoku.Analysis.Api: [getAnalysis] :: Analyses (mode :: k) -> mode :- AtAnalysis (AnalysisReply 'GET SavedAnalysis)
+ Kansoku.Analysis.Api: [listAnalyses] :: Analyses (mode :: k) -> mode :- ("analyses" :> (MetricsAuth :> (Description "Current-read pages ordered by updatedAt descending, id ascending. Default 20, maximum 100. Both directions keep canonical order. Cursors preserve microseconds, survive deletion/replacement of their source row, and must be reused with the same kind filter. No cross-request snapshot; refresh from the first page after edits. Invalid kind returns a 400 problem document; invalid pagination uses the existing Relay JSON error." :> (QueryParam "kind" AnalysisKind :> (RelayPage 20 100 :> PageReply SavedAnalysis)))))
+ Kansoku.Analysis.Api: [replaceAnalysis] :: Analyses (mode :: k) -> mode :- AtAnalysis (Description "Full replacement with required positive revision from the last read. Stale revision returns 409 analysis_revision_conflict. Every successful replacement increments revision, preserves createdAt and advances updatedAt, including identical content. The same field and byte limits as create apply." :> (ReqBody '[JSON] AnalysisReplace :> AnalysisReply 'PUT SavedAnalysis))
+ Kansoku.Analysis.Api: data Analyses (mode :: k)
+ Kansoku.Analysis.Api: data AnalysisResult a
+ Kansoku.Analysis.Api: instance GHC.Classes.Eq a => GHC.Classes.Eq (Kansoku.Analysis.Api.AnalysisResult a)
+ Kansoku.Analysis.Api: instance GHC.Internal.Generics.Generic (Kansoku.Analysis.Api.AnalysisResult a)
+ Kansoku.Analysis.Api: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Kansoku.Analysis.Api.AnalysisResult a)
+ Kansoku.Analysis.Api: instance Servant.API.MultiVerb.AsUnion (Kansoku.Analysis.Api.AnalysisResponses desc a) (Kansoku.Analysis.Api.AnalysisResult a)
+ Kansoku.Analysis.Api: instance forall k (mode :: k). GHC.Internal.Generics.Generic (Kansoku.Analysis.Api.Analyses mode)
+ Kansoku.Analysis.Api: type AnalysisReply (method :: StdMethod) a = MultiVerb method '[JSON, ProblemJSON] AnalysisResponses "Saved analysis result" a AnalysisResult a
+ Kansoku.Analysis.Api: type AnalysisResponses (desc :: Symbol) a = '[RespondAs JSON 200 desc a, RespondAs ProblemJSON 400 "Invalid analysis" ProblemDetails, RespondAs ProblemJSON 404 "Analysis not found" ProblemDetails, RespondAs ProblemJSON 409 "Revision conflict or account limit reached" ProblemDetails, RespondAs ProblemJSON 503 "Analysis store unavailable" ProblemDetails]
+ Kansoku.Analysis.Api: type AtAnalysis sub = "analyses" :> MetricsAuth :> Capture "id" UUID :> sub
+ Kansoku.Analysis.Types: AnalysisInput :: !Text -> !Maybe Text -> !AnalysisKind -> !Integer -> !Value -> AnalysisInput
+ Kansoku.Analysis.Types: AnalysisReplace :: !Text -> !Maybe Text -> !AnalysisKind -> !Integer -> !Value -> !Revision -> AnalysisReplace
+ Kansoku.Analysis.Types: Funnel :: AnalysisKind
+ Kansoku.Analysis.Types: Retention :: AnalysisKind
+ Kansoku.Analysis.Types: Revision :: Int64 -> Revision
+ Kansoku.Analysis.Types: SavedAnalysis :: !UUID -> !Text -> !Maybe Text -> !AnalysisKind -> !Integer -> !Value -> !Revision -> !UTCTime -> !UTCTime -> SavedAnalysis
+ Kansoku.Analysis.Types: Trends :: AnalysisKind
+ Kansoku.Analysis.Types: [createdAt] :: SavedAnalysis -> !UTCTime
+ Kansoku.Analysis.Types: [description] :: SavedAnalysis -> !Maybe Text
+ Kansoku.Analysis.Types: [documentVersion] :: SavedAnalysis -> !Integer
+ Kansoku.Analysis.Types: [document] :: SavedAnalysis -> !Value
+ Kansoku.Analysis.Types: [id] :: SavedAnalysis -> !UUID
+ Kansoku.Analysis.Types: [kind] :: SavedAnalysis -> !AnalysisKind
+ Kansoku.Analysis.Types: [name] :: SavedAnalysis -> !Text
+ Kansoku.Analysis.Types: [revision] :: SavedAnalysis -> !Revision
+ Kansoku.Analysis.Types: [unRevision] :: Revision -> Int64
+ Kansoku.Analysis.Types: [updatedAt] :: SavedAnalysis -> !UTCTime
+ Kansoku.Analysis.Types: data AnalysisInput
+ Kansoku.Analysis.Types: data AnalysisKind
+ Kansoku.Analysis.Types: data AnalysisReplace
+ Kansoku.Analysis.Types: data SavedAnalysis
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Analysis.Types: instance GHC.Classes.Eq Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Analysis.Types: instance GHC.Classes.Eq Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance GHC.Classes.Eq Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Analysis.Types: instance GHC.Classes.Eq Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance GHC.Classes.Eq Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Analysis.Types: instance GHC.Internal.Generics.Generic Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Analysis.Types: instance GHC.Internal.Generics.Generic Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance GHC.Internal.Generics.Generic Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Analysis.Types: instance GHC.Internal.Generics.Generic Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance GHC.Internal.Generics.Generic Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Analysis.Types: instance GHC.Internal.Show.Show Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Analysis.Types: instance GHC.Internal.Show.Show Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance GHC.Internal.Show.Show Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Analysis.Types: instance GHC.Internal.Show.Show Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance GHC.Internal.Show.Show Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Analysis.Types: instance Web.Internal.HttpApiData.FromHttpApiData Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance Web.Internal.HttpApiData.FromHttpApiData Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: instance Web.Internal.HttpApiData.ToHttpApiData Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Analysis.Types: instance Web.Internal.HttpApiData.ToHttpApiData Kansoku.Analysis.Types.Revision
+ Kansoku.Analysis.Types: kindText :: AnalysisKind -> Text
+ Kansoku.Analysis.Types: newtype Revision
+ Kansoku.Analysis.Types: replacementInput :: AnalysisReplace -> AnalysisInput
+ Kansoku.Api: [analyses] :: KansokuAPI (mode :: k) -> mode :- NamedRoutes (Analyses :: Type -> Type)
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Kansoku.Analysis.Types.Revision
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.ParamSchema.ToParamSchema Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Analysis.Types.AnalysisInput
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Analysis.Types.AnalysisKind
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Analysis.Types.AnalysisReplace
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Analysis.Types.Revision
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Analysis.Types.SavedAnalysis
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Users.Types.UserHost
+ Kansoku.Api.OpenApi: instance Data.OpenApi.Internal.Schema.ToSchema Kansoku.Users.Types.UserHosts
+ Kansoku.Metrics.Api: RetentionDay :: RetentionPeriod
+ Kansoku.Metrics.Api: RetentionMonth :: RetentionPeriod
+ Kansoku.Metrics.Api: RetentionWeek :: RetentionPeriod
+ Kansoku.Metrics.Api: data RetentionPeriod
+ Kansoku.Metrics.Api: instance GHC.Classes.Eq Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Metrics.Api: instance GHC.Internal.Generics.Generic Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Metrics.Api: instance GHC.Internal.Show.Show Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Metrics.Api: instance Web.Internal.HttpApiData.FromHttpApiData Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Metrics.Api: instance Web.Internal.HttpApiData.ToHttpApiData Kansoku.Metrics.Api.RetentionPeriod
+ Kansoku.Metrics.Limits: maxRetentionMonthOffset :: Int
+ Kansoku.Metrics.Limits: maxRetentionWeekOffset :: Int
+ Kansoku.Users.Api: [hosts] :: Users (mode :: k) -> mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("hosts" :> (Description "Complete exact host counts for resolved retained history, including identity and pre-login events. Optional paired UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in filter (1..100 names of 1..128 characters); host filters rejected. Same selection as history. Non-null hosts ordered by eventCount descending then host ascending; one null-host entry last, zero counts omitted. Sum of selected entries equals filtered history size for the same parameters on settled data; current reads without cross-request snapshot. At most 1000 entries including null, otherwise user_hosts_too_large 400 without partial output. Unknown/foreign/erased user: user_not_found 404; query failure/deadline: clickhouse_unavailable 503." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User hosts" UserHosts) (KansokuResult UserHosts))))))))
+ Kansoku.Users.Types: UserHost :: !Maybe Text -> !Integer -> !Text -> UserHost
+ Kansoku.Users.Types: UserHosts :: [UserHost] -> UserHosts
+ Kansoku.Users.Types: [eventCount] :: UserHost -> !Integer
+ Kansoku.Users.Types: [hosts] :: UserHosts -> [UserHost]
+ Kansoku.Users.Types: [lastOccurredAt] :: UserHost -> !Text
+ Kansoku.Users.Types: data UserHost
+ Kansoku.Users.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Users.Types.UserHost
+ Kansoku.Users.Types: instance Data.Aeson.Types.FromJSON.FromJSON Kansoku.Users.Types.UserHosts
+ Kansoku.Users.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Users.Types.UserHost
+ Kansoku.Users.Types: instance Data.Aeson.Types.ToJSON.ToJSON Kansoku.Users.Types.UserHosts
+ Kansoku.Users.Types: instance GHC.Classes.Eq Kansoku.Users.Types.UserHost
+ Kansoku.Users.Types: instance GHC.Classes.Eq Kansoku.Users.Types.UserHosts
+ Kansoku.Users.Types: instance GHC.Internal.Generics.Generic Kansoku.Users.Types.UserHost
+ Kansoku.Users.Types: instance GHC.Internal.Generics.Generic Kansoku.Users.Types.UserHosts
+ Kansoku.Users.Types: instance GHC.Internal.Show.Show Kansoku.Users.Types.UserHost
+ Kansoku.Users.Types: instance GHC.Internal.Show.Show Kansoku.Users.Types.UserHosts
+ Kansoku.Users.Types: newtype UserHosts
- Kansoku.Api: KansokuAPI :: (mode :- NamedRoutes (IngestRoutes :: Type -> Type)) -> (mode :- NamedRoutes (MetricsRoutes :: Type -> Type)) -> (mode :- NamedRoutes (MetricDefinitions :: Type -> Type)) -> (mode :- NamedRoutes (Users :: Type -> Type)) -> (mode :- NamedRoutes (Cohorts :: Type -> Type)) -> (mode :- SchemaAPI) -> KansokuAPI (mode :: k)
+ Kansoku.Api: KansokuAPI :: (mode :- NamedRoutes (IngestRoutes :: Type -> Type)) -> (mode :- NamedRoutes (MetricsRoutes :: Type -> Type)) -> (mode :- NamedRoutes (MetricDefinitions :: Type -> Type)) -> (mode :- NamedRoutes (Analyses :: Type -> Type)) -> (mode :- NamedRoutes (Users :: Type -> Type)) -> (mode :- NamedRoutes (Cohorts :: Type -> Type)) -> (mode :- SchemaAPI) -> KansokuAPI (mode :: k)
- Kansoku.Metrics.Api: MetricsRoutes :: (mode :- (MetricsAuth :> ("metrics" :> ("events" :> MetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("page-views" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Canonical page views with individual bot-row exclusion unless includeBots=true. Session-scoped filters/grouping on this route use session:<attribute>: channel, referrer_host, utm_source, utm_medium, utm_campaign, utm_content, utm_term, entry_page, exit_page, device_class, browser_family, os_family, country_code, event_name, source, source_service, host, route, is_bot. is_bot is Boolean; others are nullable strings. Session first page (otherwise earliest event) supplies attribution across all retained history; event dates and identities remain unchanged. Existing channel describes each event; session:channel selects every eligible event in matching visits. Missing/non-web sessions group under null and fail every session predicate, including unset/neq; real sessions with missing attributes can satisfy unset. Presence applies inside each OR alternative. Full prefixed group keys are returned; groupBy=route,session:referrer_host is supported. Nested session dimensions and other routes are excluded." :> WebMetricQuery))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("sessions" :> SessionQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("paths" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical-user paths ordered by millisecond time then event ID. by=event (default) requires start or end; by=route selects nonbot page_view rows, collapses consecutive identical routes, and permits entry mode with neither anchor. start uses the first occurrence; end uses the last and walks backwards. Anchors are mutually exclusive. steps=1..10 (default 5) counts transitions; top=1..20 (default 10) retains K transitions per step and sums the remainder into one (other) to (other) transition. window=1..7776000 seconds (default range capped at 90 days) is inclusive from the anchor. events/exclude are comma-separated event names, at most 100 each; exclude wins. Filters select rows before anchoring. Exhausted sequences emit (end), including in reverse mode. group={step,from,to}, value=users, bucket=null; order step ascending, value descending, from then to. Positive half-open range at most 366 days; complete responses, native work budgets and global 10000-point cap." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "start" Text :> (QueryParam "end" Text :> (QueryParam "steps" Int :> (QueryParam "top" Int :> (QueryParam "window" Int :> (QueryParam "events" Text :> (QueryParam "exclude" Text :> (QueryParam "by" PathBy :> (QueryParams "filter" MetricFilter :> MetricResponse)))))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("funnel" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. aggregate=users (default) or organizations; organizations merge member timelines, excluding empty organization IDs. attribution=event or user_organization selects organization attribution with first-touch property selection; user_organization fills only missing event IDs from current resolved-user membership. Existing property attribution values retain event organization attribution. Canonical subject timelines ordered by millisecond occurred_at then event_id, with immutable duplicates removed. At most 20 steps; one event cannot satisfy repeated steps. mode=any_order opens one window at the earliest matching step and counts distinct step positions; mode=sequential (default) allows intervening events; strict requires adjacency including automatic events, accepts current-user filters over complete participant timelines, and rejects event filters. counting=uniques (default) selects maximum reach, earliest completion, earliest start, then event IDs for sequential/strict. counting=totals counts nonoverlapping entries, restarting after completion, expiry or interruption; any-order entries start at any step, others at step one. Totals add an ungrouped measure=entries point per variant and label reach rows measure=count. There is no exclusion grace period. window is inclusive seconds from first step, default range capped at 7776000. Repeat exclude up to 20 distinct validated names; exclusions between matched steps stop a chain, and cannot overlap steps. measure=count (default) emits cumulative reach. time_to_convert emits exact nearest-rank p50_seconds/p90_seconds for reached steps >=2, from the same selected chain. One/two group dimensions use the earliest eligible start (per attempt for totals). prop attribution=first_touch (default), last_touch or step:<n> uses the start, last matched event or named step; absent/unreached values are other. Validate declarations in that step, or compatible declared properties across steps when touch varies; user dimensions use participant current traits. Sequential event filters select step rows and cannot hide exclusions. Positive half-open range at most 366 days; native execution/memory limits; at most 10000 complete points or HTTP 400." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam' '[Required] "steps" Text :> (QueryParam "window" Int :> (QueryParam "mode" FunnelMode :> (QueryParam "measure" FunnelMeasure :> (QueryParam "counting" FunnelCounting :> (QueryParam "attribution" Text :> (QueryParams "exclude" Text :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> (QueryParam "aggregate" AggregateBy :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult)))))))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("retention" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC days. mode=on (default) counts the exact offset day; on_or_after counts any day from offset through the largest requested offset, including unrequested intermediate days. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history, not first ever. Date bounds select the observed activity interval and cohort dates. Offset 0 always emits cohort size. Omit immature on offsets; on_or_after omits nonzero offsets unless the full maximum horizon is closed by to. to cannot exceed the current UTC day. Day grain only; filters/grouping define identical populations throughout lookback. Bot rows are excluded unless includeBots=true. Up to two dimensions, eight bounded JSON filters, 32 distinct offsets 1..365, range at most 366 days and 10000 complete points." :> MetricArguments (QueryParam "days" Text :> (QueryParam "mode" RetentionMode :> (QueryParam "cohort" CohortMode :> (QueryParam "includeBots" Bool :> OrganizationOptions MetricResponse))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("acquisition" :> AcquisitionQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("feature-adoption" :> OrganizationMetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("conversion" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact ordered signup conversion: starters are the denominator; only starters with a later signup_completed in the millisecond/event-ID timeline within range (window capped at 90 days) convert. Zero denominator returns zero; ratio stays 0..1. Group ownership is the earliest eligible start. timeGrain and prop grouping are rejected." :> OrganizationMetricQuery))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("users" :> UserQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("stickiness" :> DailyActivityQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("lifecycle" :> DailyActivityQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("revenue" :> MetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("organizations" :> OrganizationMetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("custom" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Ad-hoc or saved account-scoped metrics. Percentile requires a declared numeric property and finite quantile strictly between zero and one; exact inclusive (R-7) interpolation over canonical finite samples, independently per group/bucket. Empty populations emit no points. Quantile is forbidden on other aggregations, ratios and saved execution overrides. Existing resource limits apply. Saved defaults, ad-hoc requests and both ratio component filters accept session attributes; request filters select both sides and component filters remain side-local. Custom bot populations are unchanged. Session-scoped filters/grouping on this route use session:<attribute>: channel, referrer_host, utm_source, utm_medium, utm_campaign, utm_content, utm_term, entry_page, exit_page, device_class, browser_family, os_family, country_code, event_name, source, source_service, host, route, is_bot. is_bot is Boolean; others are nullable strings. Session first page (otherwise earliest event) supplies attribution across all retained history; event dates and identities remain unchanged. Existing channel describes each event; session:channel selects every eligible event in matching visits. Missing/non-web sessions group under null and fail every session predicate, including unset/neq; real sessions with missing attributes can satisfy unset. Presence applies inside each OR alternative. Full prefixed group keys are returned; groupBy=route,session:referrer_host is supported. Nested session dimensions and other routes are excluded." :> MetricParameters (QueryParam "metric" Text :> (QueryParam "events" Text :> (QueryParam "aggregation" Text :> (QueryParam "property" Text :> (QueryParam "quantile" Double :> (QueryParam "denominatorEvents" Text :> (QueryParam "denominatorAggregation" Text :> MetricResponse)))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Current user: filters/grouping select exact canonical populations; segmented exposures bypass the raw sketch. measure=exposures (default without segmentation) returns daily-rollup uniqCombined raw-identity exposure counts (~0.8% sketch error at scale); it differs from exact stitched users. measure=diagnostics uses exact canonical subjects and valid experiment/variant exposures, assigning each user once per experiment/requested bucket to earliest occurred_at,event_id. group.measure emits exposed_users, multiple_exposure (>1 observed variant), assigned_users and variant_share; per-variant shares sum to one when nonempty. Repeated exposures do not increase assignment. Zero users emit zero totals and no variant rows; missing/invalid names are excluded. All observed variants remain visible, including zero assignments. Day/week/month or whole range by omission; explicit hour and none rejected. First observed exposure is not an authoritative randomization log; contamination invalidates naive sample-ratio interpretation. Positive half-open range at most 366 days; at most 10000 complete points and bounded native work." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "experiment" Text :> (QueryParam "timeGrain" TimeGrain :> (QueryParam "measure" ExperimentMeasure :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> ("conversion" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Every implemented metric requires a positive half-open from/to range of at most 366 days and returns a complete answer of at most 10000 points, or a permanent HTTP 400 problem asking the caller to narrow the query. Current user: filters/grouping are supported; event selection is rejected. Per-variant conversion for one experiment: exposed unique users, users with a conversion event at or after their first exposure (earlier conversions do not count), and the rate. Users exposed to multiple variants count in each. Counts and rates only; no significance testing." :> (QueryParam' '[Required] "experiment" Text :> (QueryParam' '[Required] "event" Text :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> ("funnel" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Canonical user timelines ordered by millisecond occurred_at then event_id, with immutable duplicates removed. At most 20 steps; one event cannot satisfy repeated steps. mode=any_order opens one window at the earliest matching step and counts distinct step positions; mode=sequential (default) allows intervening events; strict requires adjacency including automatic events, accepts current-user filters over complete participant timelines, and rejects event filters. counting=uniques (default) selects maximum reach, earliest completion, earliest start, then event IDs for sequential/strict. counting=totals counts nonoverlapping entries, restarting after completion, expiry or interruption; any-order entries start at any step, others at step one. Totals add an ungrouped measure=entries point per variant and label reach rows measure=count. There is no exclusion grace period. window is inclusive seconds from first step, default range capped at 7776000. Repeat exclude up to 20 distinct validated names; exclusions between matched steps stop a chain, and cannot overlap steps. measure=count (default) emits cumulative reach. time_to_convert emits exact nearest-rank p50_seconds/p90_seconds for reached steps >=2, from the same selected chain. One/two group dimensions use the earliest eligible start (per attempt for totals). prop attribution=first_touch (default), last_touch or step:<n> uses the start, last matched event or named step; absent/unreached values are other. Validate declarations in that step, or compatible declared properties across steps when touch varies; user dimensions use participant current traits. Sequential event filters select step rows and cannot hide exclusions. Positive half-open range at most 366 days; native execution/memory limits; at most 10000 complete points or HTTP 400. Per-variant timelines include only events at or after that user/variant first exposure inside the range. Multi-variant users remain in each variant." :> (QueryParam' '[Required] "experiment" Text :> (QueryParam' '[Required] "steps" Text :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "window" Int :> (QueryParam "mode" FunnelMode :> (QueryParam "measure" FunnelMeasure :> (QueryParam "counting" FunnelCounting :> (QueryParam "attribution" FunnelAttribution :> (QueryParams "exclude" Text :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))))))))) -> MetricsRoutes (mode :: k)
+ Kansoku.Metrics.Api: MetricsRoutes :: (mode :- (MetricsAuth :> ("metrics" :> ("events" :> MetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("page-views" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Canonical page views with individual bot-row exclusion unless includeBots=true. Session-scoped filters/grouping on this route use session:<attribute>: channel, referrer_host, utm_source, utm_medium, utm_campaign, utm_content, utm_term, entry_page, exit_page, device_class, browser_family, os_family, country_code, event_name, source, source_service, host, route, is_bot. is_bot is Boolean; others are nullable strings. Session first page (otherwise earliest event) supplies attribution across all retained history; event dates and identities remain unchanged. Existing channel describes each event; session:channel selects every eligible event in matching visits. Missing/non-web sessions group under null and fail every session predicate, including unset/neq; real sessions with missing attributes can satisfy unset. Presence applies inside each OR alternative. Full prefixed group keys are returned; groupBy=route,session:referrer_host is supported. Nested session dimensions and other routes are excluded." :> WebMetricQuery))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("sessions" :> SessionQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("paths" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical-user paths ordered by millisecond time then event ID. by=event (default) requires start or end; by=route selects nonbot page_view rows, collapses consecutive identical routes, and permits entry mode with neither anchor. start uses the first occurrence; end uses the last and walks backwards. Anchors are mutually exclusive. steps=1..10 (default 5) counts transitions; top=1..20 (default 10) retains K transitions per step and sums the remainder into one (other) to (other) transition. window=1..7776000 seconds (default range capped at 90 days) is inclusive from the anchor. events/exclude are comma-separated event names, at most 100 each; exclude wins. Filters select rows before anchoring. Exhausted sequences emit (end), including in reverse mode. group={step,from,to}, value=users, bucket=null; order step ascending, value descending, from then to. Positive half-open range at most 366 days; complete responses, native work budgets and global 10000-point cap." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "start" Text :> (QueryParam "end" Text :> (QueryParam "steps" Int :> (QueryParam "top" Int :> (QueryParam "window" Int :> (QueryParam "events" Text :> (QueryParam "exclude" Text :> (QueryParam "by" PathBy :> (QueryParams "filter" MetricFilter :> MetricResponse)))))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("funnel" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. aggregate=users (default) or organizations; organizations merge member timelines, excluding empty organization IDs. attribution=event or user_organization selects organization attribution with first-touch property selection; user_organization fills only missing event IDs from current resolved-user membership. Existing property attribution values retain event organization attribution. Canonical subject timelines ordered by millisecond occurred_at then event_id, with immutable duplicates removed. At most 20 steps; one event cannot satisfy repeated steps. mode=any_order opens one window at the earliest matching step and counts distinct step positions; mode=sequential (default) allows intervening events; strict requires adjacency including automatic events, accepts current-user filters over complete participant timelines, and rejects event filters. counting=uniques (default) selects maximum reach, earliest completion, earliest start, then event IDs for sequential/strict. counting=totals counts nonoverlapping entries, restarting after completion, expiry or interruption; any-order entries start at any step, others at step one. Totals add an ungrouped measure=entries point per variant and label reach rows measure=count. There is no exclusion grace period. window is inclusive seconds from first step, default range capped at 7776000. Repeat exclude up to 20 distinct validated names; exclusions between matched steps stop a chain, and cannot overlap steps. measure=count (default) emits cumulative reach. time_to_convert emits exact nearest-rank p50_seconds/p90_seconds for reached steps >=2, from the same selected chain. One/two group dimensions use the earliest eligible start (per attempt for totals). prop attribution=first_touch (default), last_touch or step:<n> uses the start, last matched event or named step; absent/unreached values are other. Validate declarations in that step, or compatible declared properties across steps when touch varies; user dimensions use participant current traits. Sequential event filters select step rows and cannot hide exclusions. Positive half-open range at most 366 days; native execution/memory limits; at most 10000 complete points or HTTP 400." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam' '[Required] "steps" Text :> (QueryParam "window" Int :> (QueryParam "mode" FunnelMode :> (QueryParam "measure" FunnelMeasure :> (QueryParam "counting" FunnelCounting :> (QueryParam "attribution" Text :> (QueryParams "exclude" Text :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> (QueryParam "aggregate" AggregateBy :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult)))))))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("retention" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC periods. period=day (default), week (UTC Monday), or calendar month controls cohorts and days offset units independently of timeGrain (omit or day only). from must be Monday for week or day one for month; misalignment is invalid_retention_period. to may be unaligned but cannot exceed the current UTC day. Cohort labels are the period first day, YYYY-MM-DD. days is ascending distinct positive offsets: at most 32 in 1..365 days, 1..52 weeks, 1..12 months; invalid_retention_days or too_many_retention_days rejects overflow. Defaults are [1,7,14,30], [1,2,4,8], [1,3,6,12] respectively. Offset 0 always emits observed cohort size, including partial final cohorts. mode=on (default) counts once in the full target period [cohort+N,cohort+N+1); omit immature offsets. on_or_after counts once from target start through the largest requested period, including unrequested intermediate periods; omit all nonzero offsets unless that full maximum horizon is closed by to. Mature missing returns are zero. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history using start selection, not first ever. startEvent and returnEvent each independently default to any matching activity. Supplied names are trimmed single 1..128 lowercase ASCII letters, digits, underscore, dot, colon or hyphen; invalid_retention_event rejects empty/invalid names, valid unmatched names give empty starts or zero returns. Repeatable startFilter and returnFilter use the shared filter grammar and apply only on that side; shared filter applies to both. Across all three lists one eight-filter/16384 decoded UTF-8 byte budget applies (invalid_filter). Side filters accept event dimensions and governed scalar properties, including organization IDs; cohort, cohort:id, user:*, org:*, user_id and session:* terms (also inside OR) are invalid_retention_filter. Shared current traits and saved membership retain their existing contract; user_id and session selectors remain unsupported on retention. Start activity labels each group; only return activity with equal dimension values counts, including nullable groups and event_name. Groups are separate populations and need not be disjoint. Bot/profile-only exclusions apply on both sides; includeBots=true includes bot rows. Up to two dimensions, range at most 366 days, 10000 complete points; metric_result_too_large and query_resource_limit return complete errors." :> MetricArguments (QueryParam "days" Text :> (QueryParam "mode" RetentionMode :> (QueryParam "cohort" CohortMode :> (QueryParam "includeBots" Bool :> OrganizationOptions (QueryParam "period" RetentionPeriod :> (QueryParam "startEvent" Text :> (QueryParam "returnEvent" Text :> (QueryParams "startFilter" MetricFilter :> (QueryParams "returnFilter" MetricFilter :> MetricResponse)))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("acquisition" :> AcquisitionQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("feature-adoption" :> OrganizationMetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("conversion" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact ordered signup conversion: starters are the denominator; only starters with a later signup_completed in the millisecond/event-ID timeline within range (window capped at 90 days) convert. Zero denominator returns zero; ratio stays 0..1. Group ownership is the earliest eligible start. timeGrain and prop grouping are rejected." :> OrganizationMetricQuery))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("users" :> UserQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("stickiness" :> DailyActivityQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("lifecycle" :> DailyActivityQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("revenue" :> MetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("organizations" :> OrganizationMetricQuery)))) -> (mode :- (MetricsAuth :> ("metrics" :> ("custom" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Ad-hoc or saved account-scoped metrics. Percentile requires a declared numeric property and finite quantile strictly between zero and one; exact inclusive (R-7) interpolation over canonical finite samples, independently per group/bucket. Empty populations emit no points. Quantile is forbidden on other aggregations, ratios and saved execution overrides. Existing resource limits apply. Saved defaults, ad-hoc requests and both ratio component filters accept session attributes; request filters select both sides and component filters remain side-local. Custom bot populations are unchanged. Session-scoped filters/grouping on this route use session:<attribute>: channel, referrer_host, utm_source, utm_medium, utm_campaign, utm_content, utm_term, entry_page, exit_page, device_class, browser_family, os_family, country_code, event_name, source, source_service, host, route, is_bot. is_bot is Boolean; others are nullable strings. Session first page (otherwise earliest event) supplies attribution across all retained history; event dates and identities remain unchanged. Existing channel describes each event; session:channel selects every eligible event in matching visits. Missing/non-web sessions group under null and fail every session predicate, including unset/neq; real sessions with missing attributes can satisfy unset. Presence applies inside each OR alternative. Full prefixed group keys are returned; groupBy=route,session:referrer_host is supported. Nested session dimensions and other routes are excluded." :> MetricParameters (QueryParam "metric" Text :> (QueryParam "events" Text :> (QueryParam "aggregation" Text :> (QueryParam "property" Text :> (QueryParam "quantile" Double :> (QueryParam "denominatorEvents" Text :> (QueryParam "denominatorAggregation" Text :> MetricResponse)))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Current user: filters/grouping select exact canonical populations; segmented exposures bypass the raw sketch. measure=exposures (default without segmentation) returns daily-rollup uniqCombined raw-identity exposure counts (~0.8% sketch error at scale); it differs from exact stitched users. measure=diagnostics uses exact canonical subjects and valid experiment/variant exposures, assigning each user once per experiment/requested bucket to earliest occurred_at,event_id. group.measure emits exposed_users, multiple_exposure (>1 observed variant), assigned_users and variant_share; per-variant shares sum to one when nonempty. Repeated exposures do not increase assignment. Zero users emit zero totals and no variant rows; missing/invalid names are excluded. All observed variants remain visible, including zero assignments. Day/week/month or whole range by omission; explicit hour and none rejected. First observed exposure is not an authoritative randomization log; contamination invalidates naive sample-ratio interpretation. Positive half-open range at most 366 days; at most 10000 complete points and bounded native work." :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "experiment" Text :> (QueryParam "timeGrain" TimeGrain :> (QueryParam "measure" ExperimentMeasure :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> ("conversion" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Every implemented metric requires a positive half-open from/to range of at most 366 days and returns a complete answer of at most 10000 points, or a permanent HTTP 400 problem asking the caller to narrow the query. Current user: filters/grouping are supported; event selection is rejected. Per-variant conversion for one experiment: exposed unique users, users with a conversion event at or after their first exposure (earlier conversions do not count), and the rate. Users exposed to multiple variants count in each. Counts and rates only; no significance testing." :> (QueryParam' '[Required] "experiment" Text :> (QueryParam' '[Required] "event" Text :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))) -> (mode :- (MetricsAuth :> ("metrics" :> ("experiments" :> ("funnel" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Canonical user timelines ordered by millisecond occurred_at then event_id, with immutable duplicates removed. At most 20 steps; one event cannot satisfy repeated steps. mode=any_order opens one window at the earliest matching step and counts distinct step positions; mode=sequential (default) allows intervening events; strict requires adjacency including automatic events, accepts current-user filters over complete participant timelines, and rejects event filters. counting=uniques (default) selects maximum reach, earliest completion, earliest start, then event IDs for sequential/strict. counting=totals counts nonoverlapping entries, restarting after completion, expiry or interruption; any-order entries start at any step, others at step one. Totals add an ungrouped measure=entries point per variant and label reach rows measure=count. There is no exclusion grace period. window is inclusive seconds from first step, default range capped at 7776000. Repeat exclude up to 20 distinct validated names; exclusions between matched steps stop a chain, and cannot overlap steps. measure=count (default) emits cumulative reach. time_to_convert emits exact nearest-rank p50_seconds/p90_seconds for reached steps >=2, from the same selected chain. One/two group dimensions use the earliest eligible start (per attempt for totals). prop attribution=first_touch (default), last_touch or step:<n> uses the start, last matched event or named step; absent/unreached values are other. Validate declarations in that step, or compatible declared properties across steps when touch varies; user dimensions use participant current traits. Sequential event filters select step rows and cannot hide exclusions. Positive half-open range at most 366 days; native execution/memory limits; at most 10000 complete points or HTTP 400. Per-variant timelines include only events at or after that user/variant first exposure inside the range. Multi-variant users remain in each variant." :> (QueryParam' '[Required] "experiment" Text :> (QueryParam' '[Required] "steps" Text :> (QueryParam' '[Required] "from" Day :> (QueryParam' '[Required] "to" Day :> (QueryParam "window" Int :> (QueryParam "mode" FunnelMode :> (QueryParam "measure" FunnelMeasure :> (QueryParam "counting" FunnelCounting :> (QueryParam "attribution" FunnelAttribution :> (QueryParams "exclude" Text :> (QueryParam "groupBy" Text :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Metric result" MetricResult) (KansokuResult MetricResult))))))))))))))))))) -> MetricsRoutes (mode :: k)
- Kansoku.Metrics.Api: [retention] :: MetricsRoutes (mode :: k) -> mode :- (MetricsAuth :> ("metrics" :> ("retention" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC days. mode=on (default) counts the exact offset day; on_or_after counts any day from offset through the largest requested offset, including unrequested intermediate days. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history, not first ever. Date bounds select the observed activity interval and cohort dates. Offset 0 always emits cohort size. Omit immature on offsets; on_or_after omits nonzero offsets unless the full maximum horizon is closed by to. to cannot exceed the current UTC day. Day grain only; filters/grouping define identical populations throughout lookback. Bot rows are excluded unless includeBots=true. Up to two dimensions, eight bounded JSON filters, 32 distinct offsets 1..365, range at most 366 days and 10000 complete points." :> MetricArguments (QueryParam "days" Text :> (QueryParam "mode" RetentionMode :> (QueryParam "cohort" CohortMode :> (QueryParam "includeBots" Bool :> OrganizationOptions MetricResponse))))))))
+ Kansoku.Metrics.Api: [retention] :: MetricsRoutes (mode :: k) -> mode :- (MetricsAuth :> ("metrics" :> ("retention" :> (Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC periods. period=day (default), week (UTC Monday), or calendar month controls cohorts and days offset units independently of timeGrain (omit or day only). from must be Monday for week or day one for month; misalignment is invalid_retention_period. to may be unaligned but cannot exceed the current UTC day. Cohort labels are the period first day, YYYY-MM-DD. days is ascending distinct positive offsets: at most 32 in 1..365 days, 1..52 weeks, 1..12 months; invalid_retention_days or too_many_retention_days rejects overflow. Defaults are [1,7,14,30], [1,2,4,8], [1,3,6,12] respectively. Offset 0 always emits observed cohort size, including partial final cohorts. mode=on (default) counts once in the full target period [cohort+N,cohort+N+1); omit immature offsets. on_or_after counts once from target start through the largest requested period, including unrequested intermediate periods; omit all nonzero offsets unless that full maximum horizon is closed by to. Mature missing returns are zero. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history using start selection, not first ever. startEvent and returnEvent each independently default to any matching activity. Supplied names are trimmed single 1..128 lowercase ASCII letters, digits, underscore, dot, colon or hyphen; invalid_retention_event rejects empty/invalid names, valid unmatched names give empty starts or zero returns. Repeatable startFilter and returnFilter use the shared filter grammar and apply only on that side; shared filter applies to both. Across all three lists one eight-filter/16384 decoded UTF-8 byte budget applies (invalid_filter). Side filters accept event dimensions and governed scalar properties, including organization IDs; cohort, cohort:id, user:*, org:*, user_id and session:* terms (also inside OR) are invalid_retention_filter. Shared current traits and saved membership retain their existing contract; user_id and session selectors remain unsupported on retention. Start activity labels each group; only return activity with equal dimension values counts, including nullable groups and event_name. Groups are separate populations and need not be disjoint. Bot/profile-only exclusions apply on both sides; includeBots=true includes bot rows. Up to two dimensions, range at most 366 days, 10000 complete points; metric_result_too_large and query_resource_limit return complete errors." :> MetricArguments (QueryParam "days" Text :> (QueryParam "mode" RetentionMode :> (QueryParam "cohort" CohortMode :> (QueryParam "includeBots" Bool :> OrganizationOptions (QueryParam "period" RetentionPeriod :> (QueryParam "startEvent" Text :> (QueryParam "returnEvent" Text :> (QueryParams "startFilter" MetricFilter :> (QueryParams "returnFilter" MetricFilter :> MetricResponse)))))))))))))
- Kansoku.Users.Api: Users :: (mode :- ("users" :> ("activity-counts" :> (MetricsAuth :> (Description "Exact distinct identified and anonymous activity subjects. Required paired UTC dates [from,to), 1..366 days; includeBots defaults false. Excludes identify/identify_once/alias/group and empty subjects. Sum equals whole-range metrics/users with timeGrain=none. No directory filters." :> RequiredDates (QueryParam "includeBots" Bool :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Exact activity counts" UserActivityCounts) (KansokuResult UserActivityCounts))))))) -> (mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("events" :> (Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. One event_name eq/in filter, 1..100 names of 1..128 characters. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserEvent))))))))) -> (mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> (Description "Current identified-user profile. RetainedEventCount includes all event kinds and bots; activity extrema exclude bots and identify/identify_once/alias/group. Traits include winning observedAt and set/set_once provenance. Evidence counts never expose values and fingerprint validity is 180 days; resolvedAnonymousCount counts original anonymous subjects stitched to this user. Organization is current explicit membership with governed traits or null. Unknown, foreign and erased users share user_not_found 404. One deadline and owned connection, eventual cross-table consistency. Analytical inference, never authorization." :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User profile" UserProfile) (KansokuResult UserProfile)))))) -> (mode :- ("users" :> (MetricsAuth :> (Description "Identified-user Relay directory. Optional paired UTC dates [from,to), 1..366 days, select activity but extrema span all retained history. search is literal case-insensitive substring, 1..256 code points. order is lastActivity (default) or firstActivity: timestamp descending, null last, userId ascending. includeBots defaults false; identify/identify_once/alias/group are not activity. Without dates profile-only users have null extrema. Up to eight typed user trait filters and one cohort eq/neq UUID, AND only, 16384 decoded bytes. Page size 1..100, default 20, first/after or last/before. Keep selections fixed; current reads require refresh/deduplication after concurrent changes. Exhaustion is a complete error; unknown/repeated scalar parameters are rejected. Relay pagination errors use JSON, other errors problem+json." :> Dates (QueryParam "search" Text :> (QueryParam "order" Text :> (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserSummary))))))))) -> Users (mode :: k)
+ Kansoku.Users.Api: Users :: (mode :- ("users" :> ("activity-counts" :> (MetricsAuth :> (Description "Exact distinct identified and anonymous activity subjects. Required paired UTC dates [from,to), 1..366 days; includeBots defaults false. Excludes identify/identify_once/alias/group and empty subjects. Sum equals whole-range metrics/users with timeGrain=none. No directory filters." :> RequiredDates (QueryParam "includeBots" Bool :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Exact activity counts" UserActivityCounts) (KansokuResult UserActivityCounts))))))) -> (mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("events" :> (Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in term (1..100 names of 1..128 characters) and one host eq/in term (1..100 canonical lowercase hosts of 1..253 characters without scheme, port, path, whitespace or trailing dot), ANDed. Host matches node host; null hosts never match a host term. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserEvent))))))))) -> (mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("hosts" :> (Description "Complete exact host counts for resolved retained history, including identity and pre-login events. Optional paired UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in filter (1..100 names of 1..128 characters); host filters rejected. Same selection as history. Non-null hosts ordered by eventCount descending then host ascending; one null-host entry last, zero counts omitted. Sum of selected entries equals filtered history size for the same parameters on settled data; current reads without cross-request snapshot. At most 1000 entries including null, otherwise user_hosts_too_large 400 without partial output. Unknown/foreign/erased user: user_not_found 404; query failure/deadline: clickhouse_unavailable 503." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User hosts" UserHosts) (KansokuResult UserHosts))))))))) -> (mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> (Description "Current identified-user profile. RetainedEventCount includes all event kinds and bots; activity extrema exclude bots and identify/identify_once/alias/group. Traits include winning observedAt and set/set_once provenance. Evidence counts never expose values and fingerprint validity is 180 days; resolvedAnonymousCount counts original anonymous subjects stitched to this user. Organization is current explicit membership with governed traits or null. Unknown, foreign and erased users share user_not_found 404. One deadline and owned connection, eventual cross-table consistency. Analytical inference, never authorization." :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User profile" UserProfile) (KansokuResult UserProfile)))))) -> (mode :- ("users" :> (MetricsAuth :> (Description "Identified-user Relay directory. Optional paired UTC dates [from,to), 1..366 days, select activity but extrema span all retained history. search is literal case-insensitive substring, 1..256 code points. order is lastActivity (default) or firstActivity: timestamp descending, null last, userId ascending. includeBots defaults false; identify/identify_once/alias/group are not activity. Without dates profile-only users have null extrema. Up to eight typed user trait filters and one cohort eq/neq UUID, AND only, 16384 decoded bytes. Page size 1..100, default 20, first/after or last/before. Keep selections fixed; current reads require refresh/deduplication after concurrent changes. Exhaustion is a complete error; unknown/repeated scalar parameters are rejected. Relay pagination errors use JSON, other errors problem+json." :> Dates (QueryParam "search" Text :> (QueryParam "order" Text :> (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserSummary))))))))) -> Users (mode :: k)
- Kansoku.Users.Api: [events] :: Users (mode :: k) -> mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("events" :> (Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. One event_name eq/in filter, 1..100 names of 1..128 characters. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserEvent))))))))
+ Kansoku.Users.Api: [events] :: Users (mode :: k) -> mode :- ("users" :> (MetricsAuth :> (Capture "userId" Text :> ("events" :> (Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in term (1..100 names of 1..128 characters) and one host eq/in term (1..100 canonical lowercase hosts of 1..253 characters without scheme, port, path, whitespace or trailing dot), ANDed. Host matches node host; null hosts never match a host term. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> (QueryParams "filter" MetricFilter :> (RelayPage 20 100 :> PageReply UserEvent))))))))
- Kansoku.Users.Types: [host] :: UserEvent -> !Maybe Text
+ Kansoku.Users.Types: [host] :: UserHost -> !Maybe Text

Files

CHANGELOG.md view
@@ -1,5 +1,24 @@ # Changelog +## 0.3.0.0 — 2026-11-14++### Breaking Changes++- Add the `analyses` route group to `KansokuAPI` and the `hosts` route to `Users`, changing the named route record types.+- Extend the retention route with `period`, `startEvent`, `returnEvent`, and separate start and return filter parameters.++### New Features++- Add `Kansoku.Analysis.Api` and `Kansoku.Analysis.Types` for revisioned, account-owned opaque analysis documents with keyset pagination.+- Add `RetentionPeriod` for exact UTC day, Monday-week, and calendar-month retention cohorts.+- Add `UserHost` and `UserHosts` for exact per-host counts over a user's retained history, and allow filtering user event history by host.+- Add analysis problem types, including `409` revision-conflict and limit responses, to the problem catalog and generated OpenAPI.++### Bug Fixes++- Document analysis collection validation problems separately from Relay pagination errors.+- Keep the analysis document capacity independent of the configured ingest body limit.+ ## 0.2.0.0 — 2026-11-12  ### Breaking Changes
kansoku-api.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kansoku-api-version: 0.2.0.0+version: 0.3.0.0 synopsis: Servant API and wire DTO types for Kansoku. description:   Shared Servant route types, analytics event types, metric DTOs, validation,@@ -32,6 +32,8 @@   import: common   hs-source-dirs: src   exposed-modules:+    Kansoku.Analysis.Api+    Kansoku.Analysis.Types     Kansoku.Api     Kansoku.Api.OpenApi     Kansoku.Api.Response
+ src/Kansoku/Analysis/Api.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE UndecidableInstances #-}++module Kansoku.Analysis.Api where++import Data.SOP (I (..), NS (..))+import Data.UUID (UUID)+import GHC.TypeLits (Symbol)+import Kansoku.Analysis.Types+import Kansoku.Api.Response+import Kansoku.Auth.Api (MetricsAuth)+import Kansoku.Cohort.Api (PageReply)+import Kansoku.Prelude+import Relay.Pagination.Servant (RelayPage)+import Servant.API+import Servant.API.MultiVerb++data AnalysisResult a+  = AnalysisOk a+  | AnalysisBadRequest ProblemDetails+  | AnalysisNotFound ProblemDetails+  | AnalysisConflict ProblemDetails+  | AnalysisUnavailable ProblemDetails+  deriving stock (Generic, Eq, Show)++type AnalysisResponses (desc :: Symbol) a =+  '[ RespondAs JSON 200 desc a,+     RespondAs ProblemJSON 400 "Invalid analysis" ProblemDetails,+     RespondAs ProblemJSON 404 "Analysis not found" ProblemDetails,+     RespondAs ProblemJSON 409 "Revision conflict or account limit reached" ProblemDetails,+     RespondAs ProblemJSON 503 "Analysis store unavailable" ProblemDetails+   ]++instance AsUnion (AnalysisResponses desc a) (AnalysisResult a) where+  toUnion = \case+    AnalysisOk x -> Z (I x)+    AnalysisBadRequest x -> S (Z (I x))+    AnalysisNotFound x -> S (S (Z (I x)))+    AnalysisConflict x -> S (S (S (Z (I x))))+    AnalysisUnavailable x -> S (S (S (S (Z (I x)))))+  fromUnion = \case+    Z (I x) -> AnalysisOk x+    S (Z (I x)) -> AnalysisBadRequest x+    S (S (Z (I x))) -> AnalysisNotFound x+    S (S (S (Z (I x)))) -> AnalysisConflict x+    S (S (S (S (Z (I x))))) -> AnalysisUnavailable x+    S (S (S (S (S impossible)))) -> case impossible of {}++type AtAnalysis sub = "analyses" :> MetricsAuth :> Capture "id" UUID :> sub++type AnalysisReply method a = MultiVerb method '[JSON, ProblemJSON] (AnalysisResponses "Saved analysis result" a) (AnalysisResult a)++data Analyses mode = Analyses+  { createAnalysis :: mode :- "analyses" :> MetricsAuth :> Description "Save opaque client state without executing or interpreting it. Names are trimmed, 1..120 Unicode characters; descriptions retain whitespace, maximum 500 characters. Metadata rejects zero characters. documentVersion is a positive integer; document must be an object, maximum 32768 compact Aeson UTF-8 bytes. Raw analysis request bodies have a separate 5 MiB transport limit, independent of the ingest body setting. Duplicate names are allowed. The positive KANSOKU_ANALYSES_MAX_PER_ACCOUNT limit defaults to 1000 and must match on all replicas. A full account returns non-retryable 409 analysis_limit_reached." :> ReqBody '[JSON] AnalysisInput :> AnalysisReply 'POST SavedAnalysis,+    getAnalysis :: mode :- AtAnalysis (AnalysisReply 'GET SavedAnalysis),+    listAnalyses :: mode :- "analyses" :> MetricsAuth :> Description "Current-read pages ordered by updatedAt descending, id ascending. Default 20, maximum 100. Both directions keep canonical order. Cursors preserve microseconds, survive deletion/replacement of their source row, and must be reused with the same kind filter. No cross-request snapshot; refresh from the first page after edits. Invalid kind returns a 400 problem document; invalid pagination uses the existing Relay JSON error." :> QueryParam "kind" AnalysisKind :> RelayPage 20 100 :> PageReply SavedAnalysis,+    replaceAnalysis :: mode :- AtAnalysis (Description "Full replacement with required positive revision from the last read. Stale revision returns 409 analysis_revision_conflict. Every successful replacement increments revision, preserves createdAt and advances updatedAt, including identical content. The same field and byte limits as create apply." :> ReqBody '[JSON] AnalysisReplace :> AnalysisReply 'PUT SavedAnalysis),+    deleteAnalysis :: mode :- AtAnalysis (Description "Delete with the required positive revision from the last read. Stale revision returns 409; missing or foreign IDs return 404, including repeated deletion." :> QueryParam' '[Required, Strict] "revision" Revision :> AnalysisReply 'DELETE Bool)+  }+  deriving stock (Generic)
+ src/Kansoku/Analysis/Types.hs view
@@ -0,0 +1,85 @@+-- | Opaque, account-owned client documents. Kansoku never executes these values.+module Kansoku.Analysis.Types where++import Data.Aeson qualified as A+import Data.Int (Int64)+import Data.Text qualified as Text+import Data.UUID (UUID)+import Kansoku.Prelude+import Servant.API (FromHttpApiData (..), ToHttpApiData (..))+import Text.Read (readMaybe)++data AnalysisKind = Trends | Funnel | Retention+  deriving stock (Generic, Eq, Show)++kindText :: AnalysisKind -> Text+kindText = \case Trends -> "trends"; Funnel -> "funnel"; Retention -> "retention"++instance FromHttpApiData AnalysisKind where+  parseUrlPiece = \case+    "trends" -> Right Trends+    "funnel" -> Right Funnel+    "retention" -> Right Retention+    _ -> Left "kind: expected trends, funnel, or retention"++instance ToHttpApiData AnalysisKind where toUrlPiece = kindText++instance ToJSON AnalysisKind where toJSON = A.String . kindText++instance FromJSON AnalysisKind where+  parseJSON = A.withText "kind" (either (fail . Text.unpack) pure . parseUrlPiece)++-- | PostgreSQL bigint, always positive. Increment is checked by the store.+newtype Revision = Revision {unRevision :: Int64}+  deriving stock (Generic, Eq, Show)+  deriving newtype (ToJSON)++instance FromJSON Revision where+  parseJSON value = do+    n <- parseJSON value+    if n > 0 then pure (Revision n) else fail "revision: expected a positive bigint"++instance FromHttpApiData Revision where+  parseUrlPiece raw = case readMaybe (Text.unpack raw) of+    Just n | n > 0 -> Right (Revision n)+    _ -> Left "revision: expected a positive bigint"++instance ToHttpApiData Revision where toUrlPiece = Text.pack . show . unRevision++data AnalysisInput = AnalysisInput+  { name :: !Text,+    description :: !(Maybe Text),+    kind :: !AnalysisKind,+    documentVersion :: !Integer,+    document :: !A.Value+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromJSON, ToJSON)++data AnalysisReplace = AnalysisReplace+  { name :: !Text,+    description :: !(Maybe Text),+    kind :: !AnalysisKind,+    documentVersion :: !Integer,+    document :: !A.Value,+    revision :: !Revision+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromJSON, ToJSON)++replacementInput :: AnalysisReplace -> AnalysisInput+replacementInput AnalysisReplace {name, description, kind, documentVersion, document} = AnalysisInput {name, description, kind, documentVersion, document}++data SavedAnalysis = SavedAnalysis+  { id :: !UUID,+    name :: !Text,+    description :: !(Maybe Text),+    kind :: !AnalysisKind,+    documentVersion :: !Integer,+    document :: !A.Value,+    revision :: !Revision,+    createdAt :: !UTCTime,+    updatedAt :: !UTCTime+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromJSON, ToJSON)
src/Kansoku/Api.hs view
@@ -6,6 +6,7 @@   ) where +import Kansoku.Analysis.Api (Analyses) import Kansoku.Cohort.Api (Cohorts) import Kansoku.Event.Contract (SchemaCatalog) import Kansoku.Ingest.Api (IngestRoutes)@@ -21,6 +22,7 @@   { ingest :: mode :- NamedRoutes IngestRoutes,     metrics :: mode :- NamedRoutes MetricsRoutes,     metricDefinitions :: mode :- NamedRoutes MetricDefinitions,+    analyses :: mode :- NamedRoutes Analyses,     users :: mode :- NamedRoutes Users,     cohorts :: mode :- NamedRoutes Cohorts,     schemas :: mode :- SchemaAPI
src/Kansoku/Api/OpenApi.hs view
@@ -10,9 +10,11 @@  import Data.Aeson qualified as A import Data.Char (toLower, toUpper)+import Data.Int (Int64) import Data.OpenApi qualified as O import Data.OpenApi.Declare (runDeclare) import Data.Text qualified as Text+import Kansoku.Analysis.Types import Kansoku.Api (kansokuAPI) import Kansoku.Api.Response (ProblemDetails, problemFieldName) import Kansoku.Cohort.Types@@ -35,7 +37,7 @@     VersionedEventSchema,   ) import Kansoku.Ingest.Api (BatchAck, BeaconEnvelope, IngestAck, IngestEvent, RejectedEvent)-import Kansoku.Metrics.Api (AcquisitionMeasure, AggregateBy, CohortMode, ExperimentMeasure, FunnelAttribution, FunnelCounting, FunnelMeasure, FunnelMode, MetricPoint, MetricResult, OrganizationAttribution, PathBy, RetentionMode, SessionMeasure, TimeGrain, UserWindow)+import Kansoku.Metrics.Api (AcquisitionMeasure, AggregateBy, CohortMode, ExperimentMeasure, FunnelAttribution, FunnelCounting, FunnelMeasure, FunnelMode, MetricPoint, MetricResult, OrganizationAttribution, PathBy, RetentionMode, RetentionPeriod, SessionMeasure, TimeGrain, UserWindow) import Kansoku.Metrics.Definition import Kansoku.Metrics.Filter (FilterOp, MetricFilter) import Kansoku.Metrics.Limits (maxMetricPoints)@@ -197,6 +199,9 @@ instance O.ToParamSchema UserWindow where   toParamSchema _ = O.toParamSchema (Proxy @Text) & O.enum_ ?~ ["calendar", "rolling7", "rolling30"] +instance O.ToParamSchema RetentionPeriod where+  toParamSchema _ = O.toParamSchema (Proxy @Text) & O.enum_ ?~ ["day", "week", "month"]+ instance O.ToParamSchema RetentionMode where   toParamSchema _ = O.toParamSchema (Proxy @Text) & O.enum_ ?~ ["on", "on_or_after"] @@ -275,7 +280,7 @@     & withUnsupportedBodyMedia     & withRetryAfterHeader     & withOperationIds-    & withUserPageProblems+    & withSelectionPageProblems  authSecuritySchemes :: O.SecurityDefinitions authSecuritySchemes =@@ -498,6 +503,16 @@     named <- O.genericDeclareNamedSchema O.defaultSchemaOptions proxy     pure (nullableUserFields ["firstActivityAt", "lastActivityAt"] named & O.schema . O.required .~ ["userId", "traits", "firstActivityAt", "lastActivityAt"]) +instance O.ToSchema UserHosts where+  declareNamedSchema proxy = do+    named <- O.genericDeclareNamedSchema O.defaultSchemaOptions proxy+    pure (named & O.schema . O.properties . ix "hosts" %~ (\case O.Inline schema -> O.Inline (schema & O.maxItems ?~ 1000); reference -> reference))++instance O.ToSchema UserHost where+  declareNamedSchema proxy = do+    named <- O.genericDeclareNamedSchema O.defaultSchemaOptions proxy+    pure (nullableUserFields ["host"] named & O.schema . O.required .~ ["host", "eventCount", "lastOccurredAt"])+ instance O.ToSchema UserActivityCounts  instance O.ToSchema A.Value where@@ -542,27 +557,73 @@   where     addNullable field result = result & O.schema . O.properties . at field ?~ O.Inline (mempty & O.type_ ?~ O.OpenApiTypeArray [O.OpenApiString, O.OpenApiNull]) --- User selections also fail before Relay parsing, with the shared problem body.-withUserPageProblems :: O.OpenApi -> O.OpenApi-withUserPageProblems =+-- User selections and analysis kinds can fail before Relay parsing, with a problem body.+withSelectionPageProblems :: O.OpenApi -> O.OpenApi+withSelectionPageProblems =   O.paths     %~ imap       ( \path item ->-          if path `elem` ["/users", "/users/{userId}/events"]+          if path `elem` ["/users", "/users/{userId}/events", "/analyses"]             then               item                 & O.get                 . _Just                 %~ ( \operation -> case operation ^. at 400 of-                       Just (O.Inline response) -> pageBounds operation & at 400 ?~ O.Inline (response & O.content . at "application/problem+json" ?~ (mempty & O.schema ?~ problemSchema))-                       _ -> pageBounds operation+                       Just (O.Inline response) -> bounds path operation & at 400 ?~ O.Inline (response & O.content . at "application/problem+json" ?~ (mempty & O.schema ?~ problemSchema))+                       _ -> bounds path operation                    )             else item       )   where     (_, problemSchema) = runDeclare (O.declareSchemaRef (Proxy @ProblemDetails)) mempty +    bounds "/analyses" operation = operation+    bounds _ operation = pageBounds operation+     pageBounds operation = operation & O.parameters %~ map bound     bound (O.Inline parameter)       | parameter ^. O.name `elem` ["first", "last"] = O.Inline (parameter & O.schema . _Just %~ (\case O.Inline schema -> O.Inline (schema & O.minimum_ ?~ 1); reference -> reference))     bound parameter = parameter++instance O.ToSchema AnalysisKind where+  declareNamedSchema = O.genericDeclareNamedSchema lowerEnumOptions++instance O.ToParamSchema AnalysisKind where+  toParamSchema = O.genericToParamSchema lowerEnumOptions++instance O.ToSchema Revision where+  declareNamedSchema _ = pure (O.NamedSchema (Just "Revision") (O.toSchema (Proxy @Int64) & O.minimum_ ?~ 1))++instance O.ToParamSchema Revision where+  toParamSchema _ = O.toParamSchema (Proxy @Int64) & O.minimum_ ?~ 1++instance O.ToSchema AnalysisInput where+  declareNamedSchema proxy = analysisEnvelopeSchema <$> O.genericDeclareNamedSchema O.defaultSchemaOptions proxy++instance O.ToSchema AnalysisReplace where+  declareNamedSchema proxy = analysisEnvelopeSchema <$> O.genericDeclareNamedSchema O.defaultSchemaOptions proxy++instance O.ToSchema SavedAnalysis where+  declareNamedSchema proxy = do+    named <- O.genericDeclareNamedSchema O.defaultSchemaOptions proxy+    pure (analysisEnvelopeSchema named & O.schema . O.required %~ ("description" :))++analysisEnvelopeSchema :: O.NamedSchema -> O.NamedSchema+analysisEnvelopeSchema named =+  named+    & O.schema+    . O.properties+    . ix "name"+    .~ O.Inline (mempty & O.type_ ?~ O.OpenApiTypeSingle O.OpenApiString & O.minLength ?~ 1 & O.maxLength ?~ 120 & O.description ?~ "Trimmed name; Unicode character count; no zero characters")+    & O.schema+    . O.properties+    . ix "document"+    .~ O.Inline (mempty & O.type_ ?~ O.OpenApiTypeSingle O.OpenApiObject & O.additionalProperties ?~ O.AdditionalPropertiesAllowed True & O.description ?~ "Opaque JSON object, never executed or interpreted. Maximum 32768 bytes of compact Aeson UTF-8 encoding; nested values, including escaped zero characters, are preserved.")+    & O.schema+    . O.properties+    . ix "documentVersion"+    .~ O.Inline (mempty & O.type_ ?~ O.OpenApiTypeSingle O.OpenApiInteger & O.minimum_ ?~ 1 & O.description ?~ "Positive integral client document version")+    & O.schema+    . O.properties+    . ix "description"+    %~ (\case O.Inline schema -> O.Inline (schema & O.type_ ?~ O.OpenApiTypeArray [O.OpenApiString, O.OpenApiNull] & O.maxLength ?~ 500 & O.description ?~ "Optional or null; whitespace retained; maximum 500 Unicode characters; no zero characters"); reference -> reference)
src/Kansoku/Api/Response.hs view
@@ -27,7 +27,7 @@ import GHC.TypeLits (Symbol) import Kansoku.Prelude import Network.HTTP.Media ((//))-import Network.HTTP.Types.Status (status400, status401, status403, status404, status405, status406, status413, status415, status429, status500, status503, statusMessage)+import Network.HTTP.Types.Status (status400, status401, status403, status404, status405, status406, status409, status413, status415, status429, status500, status503, statusMessage) import Servant.API (Accept (..), Header, JSON, MimeRender (..), MimeUnrender (..)) import Servant.API.MultiVerb (AsUnion (..), RespondAs, WithHeaders) @@ -171,6 +171,7 @@                   404 -> status404                   405 -> status405                   406 -> status406+                  409 -> status409                   413 -> status413                   415 -> status415                   429 -> status429@@ -197,7 +198,12 @@ -- from 'problem'; human-readable detail is supplied by the failure producer. problemCatalog :: [(Text, Int)] problemCatalog =-  [ ("invalid_cohort", 400),+  [ ("invalid_analysis", 400),+    ("analysis_not_found", 404),+    ("analysis_revision_conflict", 409),+    ("analysis_limit_reached", 409),+    ("analysis_store_unavailable", 503),+    ("invalid_cohort", 400),     ("cohort_not_found", 404),     ("cohort_store_unavailable", 503),     ("invalid_date_range", 400),
src/Kansoku/Metrics/Api.hs view
@@ -8,6 +8,7 @@     SessionMeasure (..),     UserWindow (..),     RetentionMode (..),+    RetentionPeriod (..),     CohortMode (..),     PathBy (..),     FunnelMode (..),@@ -171,6 +172,21 @@   toUrlPiece Rolling7 = "rolling7"   toUrlPiece Rolling30 = "rolling30" +-- | UTC calendar unit for retention cohorts and offsets, independent of output grain.+data RetentionPeriod = RetentionDay | RetentionWeek | RetentionMonth+  deriving stock (Generic, Eq, Show)++instance FromHttpApiData RetentionPeriod where+  parseUrlPiece "day" = Right RetentionDay+  parseUrlPiece "week" = Right RetentionWeek+  parseUrlPiece "month" = Right RetentionMonth+  parseUrlPiece _ = Left "period must be day, week or month"++instance ToHttpApiData RetentionPeriod where+  toUrlPiece RetentionDay = "day"+  toUrlPiece RetentionWeek = "week"+  toUrlPiece RetentionMonth = "month"+ data RetentionMode = RetentionOn | RetentionOnOrAfter   deriving stock (Generic, Eq, Show) @@ -361,8 +377,8 @@         :- MetricsAuth           :> "metrics"           :> "retention"-          :> Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC days. mode=on (default) counts the exact offset day; on_or_after counts any day from offset through the largest requested offset, including unrequested intermediate days. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history, not first ever. Date bounds select the observed activity interval and cohort dates. Offset 0 always emits cohort size. Omit immature on offsets; on_or_after omits nonzero offsets unless the full maximum horizon is closed by to. to cannot exceed the current UTC day. Day grain only; filters/grouping define identical populations throughout lookback. Bot rows are excluded unless includeBots=true. Up to two dimensions, eight bounded JSON filters, 32 distinct offsets 1..365, range at most 366 days and 10000 complete points."-          :> MetricArguments (QueryParam "days" Text :> QueryParam "mode" RetentionMode :> QueryParam "cohort" CohortMode :> QueryParam "includeBots" Bool :> OrganizationOptions MetricResponse),+          :> Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact canonical retention over closed UTC periods. period=day (default), week (UTC Monday), or calendar month controls cohorts and days offset units independently of timeGrain (omit or day only). from must be Monday for week or day one for month; misalignment is invalid_retention_period. to may be unaligned but cannot exceed the current UTC day. Cohort labels are the period first day, YYYY-MM-DD. days is ascending distinct positive offsets: at most 32 in 1..365 days, 1..52 weeks, 1..12 months; invalid_retention_days or too_many_retention_days rejects overflow. Defaults are [1,7,14,30], [1,2,4,8], [1,3,6,12] respectively. Offset 0 always emits observed cohort size, including partial final cohorts. mode=on (default) counts once in the full target period [cohort+N,cohort+N+1); omit immature offsets. on_or_after counts once from target start through the largest requested period, including unrequested intermediate periods; omit all nonzero offsets unless that full maximum horizon is closed by to. Mature missing returns are zero. cohort=window (default) means first matching activity in range; lifetime means first matching activity in retained history using start selection, not first ever. startEvent and returnEvent each independently default to any matching activity. Supplied names are trimmed single 1..128 lowercase ASCII letters, digits, underscore, dot, colon or hyphen; invalid_retention_event rejects empty/invalid names, valid unmatched names give empty starts or zero returns. Repeatable startFilter and returnFilter use the shared filter grammar and apply only on that side; shared filter applies to both. Across all three lists one eight-filter/16384 decoded UTF-8 byte budget applies (invalid_filter). Side filters accept event dimensions and governed scalar properties, including organization IDs; cohort, cohort:id, user:*, org:*, user_id and session:* terms (also inside OR) are invalid_retention_filter. Shared current traits and saved membership retain their existing contract; user_id and session selectors remain unsupported on retention. Start activity labels each group; only return activity with equal dimension values counts, including nullable groups and event_name. Groups are separate populations and need not be disjoint. Bot/profile-only exclusions apply on both sides; includeBots=true includes bot rows. Up to two dimensions, range at most 366 days, 10000 complete points; metric_result_too_large and query_resource_limit return complete errors."+          :> MetricArguments (QueryParam "days" Text :> QueryParam "mode" RetentionMode :> QueryParam "cohort" CohortMode :> QueryParam "includeBots" Bool :> OrganizationOptions (QueryParam "period" RetentionPeriod :> QueryParam "startEvent" Text :> QueryParam "returnEvent" Text :> QueryParams "startFilter" MetricFilter :> QueryParams "returnFilter" MetricFilter :> MetricResponse)),     acquisition :: mode :- MetricsAuth :> "metrics" :> "acquisition" :> AcquisitionQuery,     featureAdoption :: mode :- MetricsAuth :> "metrics" :> "feature-adoption" :> OrganizationMetricQuery,     conversion :: mode :- MetricsAuth :> "metrics" :> "conversion" :> Description "user_id is filter-only: eq/neq accept one nonempty opaque string; in accepts 1..20 raw string IDs before deduplication (1024 UTF-8 bytes each). Only identified subjects match, including neq; anonymous/missing subjects fail each term. Resolved event identity includes pre-login history; sessions/acquisition select the first resolved user and retain the whole session. Supported only on events, page-views, users (calendar/rolling7/rolling30), sessions, acquisition, revenue, feature-adoption and custom. Other routes return invalid_metric; groupBy=user_id returns invalid_group_by. Saved simple defaults AND with requests; ratio component selection stays side-local. Existing route grains, populations, budgets and 10000 complete-point cap apply; selecting one user is not a keyed lookup. Exact ordered signup conversion: starters are the denominator; only starters with a later signup_completed in the millisecond/event-ID timeline within range (window capped at 90 days) convert. Zero denominator returns zero; ratio stays 0..1. Group ownership is the earliest eligible start. timeGrain and prop grouping are rejected." :> OrganizationMetricQuery,
src/Kansoku/Metrics/Limits.hs view
@@ -7,6 +7,8 @@     maxFunnelSteps,     maxRetentionDays,     maxRetentionOffset,+    maxRetentionWeekOffset,+    maxRetentionMonthOffset,     maxMetricPoints,     maxPathSteps,     maxPathTop,@@ -38,3 +40,7 @@  maxSelectedUserIds :: Int maxSelectedUserIds = 20++maxRetentionWeekOffset, maxRetentionMonthOffset :: Int+maxRetentionWeekOffset = 52+maxRetentionMonthOffset = 12
src/Kansoku/Users/Api.hs view
@@ -19,7 +19,8 @@  data Users mode = Users   { activityCounts :: mode :- "users" :> "activity-counts" :> MetricsAuth :> Description "Exact distinct identified and anonymous activity subjects. Required paired UTC dates [from,to), 1..366 days; includeBots defaults false. Excludes identify/identify_once/alias/group and empty subjects. Sum equals whole-range metrics/users with timeGrain=none. No directory filters." :> RequiredDates (QueryParam "includeBots" Bool :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "Exact activity counts" UserActivityCounts) (KansokuResult UserActivityCounts)),-    events :: mode :- "users" :> MetricsAuth :> Capture "userId" Text :> "events" :> Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. One event_name eq/in filter, 1..100 names of 1..128 characters. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> QueryParams "filter" MetricFilter :> RelayPage 20 100 :> PageReply UserEvent),+    events :: mode :- "users" :> MetricsAuth :> Capture "userId" Text :> "events" :> Description "Resolved retained history including pre-login and identity events. Paired optional UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in term (1..100 names of 1..128 characters) and one host eq/in term (1..100 canonical lowercase hosts of 1..253 characters without scheme, port, path, whitespace or trailing dot), ANDed. Host matches node host; null hosts never match a host term. Occurrence/receipt/event ID descending. Exact-version declared scalar properties only, excluding identifiers, alias payloads and context. Identity/group properties require governed catalogs. Relay page size 1..100, default 20; both directions, current reads without snapshot. Unknown/foreign/erased user: user_not_found 404. Nonpagination errors use problem+json; Relay pagination errors use JSON." :> Dates (QueryParam "includeBots" Bool :> QueryParams "filter" MetricFilter :> RelayPage 20 100 :> PageReply UserEvent),+    hosts :: mode :- "users" :> MetricsAuth :> Capture "userId" Text :> "hosts" :> Description "Complete exact host counts for resolved retained history, including identity and pre-login events. Optional paired UTC dates [from,to), 1..366 days; includeBots defaults false. At most one event_name eq/in filter (1..100 names of 1..128 characters); host filters rejected. Same selection as history. Non-null hosts ordered by eventCount descending then host ascending; one null-host entry last, zero counts omitted. Sum of selected entries equals filtered history size for the same parameters on settled data; current reads without cross-request snapshot. At most 1000 entries including null, otherwise user_hosts_too_large 400 without partial output. Unknown/foreign/erased user: user_not_found 404; query failure/deadline: clickhouse_unavailable 503." :> Dates (QueryParam "includeBots" Bool :> QueryParams "filter" MetricFilter :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User hosts" UserHosts) (KansokuResult UserHosts)),     getProfile :: mode :- "users" :> MetricsAuth :> Capture "userId" Text :> Description "Current identified-user profile. RetainedEventCount includes all event kinds and bots; activity extrema exclude bots and identify/identify_once/alias/group. Traits include winning observedAt and set/set_once provenance. Evidence counts never expose values and fingerprint validity is 180 days; resolvedAnonymousCount counts original anonymous subjects stitched to this user. Organization is current explicit membership with governed traits or null. Unknown, foreign and erased users share user_not_found 404. One deadline and owned connection, eventual cross-table consistency. Analytical inference, never authorization." :> MultiVerb 'GET '[JSON, ProblemJSON] (OkResponses "User profile" UserProfile) (KansokuResult UserProfile),     directory :: mode :- "users" :> MetricsAuth :> Description "Identified-user Relay directory. Optional paired UTC dates [from,to), 1..366 days, select activity but extrema span all retained history. search is literal case-insensitive substring, 1..256 code points. order is lastActivity (default) or firstActivity: timestamp descending, null last, userId ascending. includeBots defaults false; identify/identify_once/alias/group are not activity. Without dates profile-only users have null extrema. Up to eight typed user trait filters and one cohort eq/neq UUID, AND only, 16384 decoded bytes. Page size 1..100, default 20, first/after or last/before. Keep selections fixed; current reads require refresh/deduplication after concurrent changes. Exhaustion is a complete error; unknown/repeated scalar parameters are rejected. Relay pagination errors use JSON, other errors problem+json." :> Dates (QueryParam "search" Text :> QueryParam "order" Text :> QueryParam "includeBots" Bool :> QueryParams "filter" MetricFilter :> RelayPage 20 100 :> PageReply UserSummary)   }
src/Kansoku/Users/Types.hs view
@@ -74,3 +74,27 @@       <*> o Data.Aeson..: "isPageView"       <*> o Data.Aeson..: "properties"       <*> pure ""++data UserHost = UserHost+  { host :: !(Maybe Text),+    eventCount :: !Integer,+    lastOccurredAt :: !Text+  }+  deriving stock (Generic, Eq, Show)++instance ToJSON UserHost where+  toJSON UserHost {host, eventCount, lastOccurredAt} =+    Data.Aeson.object ["host" Data.Aeson..= host, "eventCount" Data.Aeson..= eventCount, "lastOccurredAt" Data.Aeson..= lastOccurredAt]++instance FromJSON UserHost where+  parseJSON = Data.Aeson.withObject "user host" $ \o ->+    UserHost <$> o Data.Aeson..: "host" <*> o Data.Aeson..: "eventCount" <*> o Data.Aeson..: "lastOccurredAt"++newtype UserHosts = UserHosts {hosts :: [UserHost]}+  deriving stock (Generic, Eq, Show)++instance ToJSON UserHosts where+  toJSON UserHosts {hosts} = Data.Aeson.object ["hosts" Data.Aeson..= hosts]++instance FromJSON UserHosts where+  parseJSON = Data.Aeson.withObject "user hosts" $ \o -> UserHosts <$> o Data.Aeson..: "hosts"