Compare commits

..

5 Commits

Author SHA1 Message Date
e3524cab97 Merge pull request 'Add weekly cohort funnel to getUsageStats' (#293) from dean/usage-stats-cohorts into master
Reviewed-on: #293
2026-07-12 04:07:43 +00:00
Dean Wenstrand
35c7c23ba9 Add disjoint activated segment to cohorts for stacking
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:45:01 -07:00
Dean Wenstrand
40c4b82a59 Add weekly cohort funnel to getUsageStats
All checks were successful
Tests / Tests (pull_request) Successful in 13s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:40:10 -07:00
9ebe2e61df Merge pull request 'dean/usage-stats' (#292) from dean/usage-stats into master
Reviewed-on: #292
2026-07-11 19:26:13 +00:00
dc02fd8387 Merge pull request 'Select detected ball colors for Shot Lab' (#291) from colonelpanic/shot-lab-image-colors into master
Reviewed-on: #291
2026-07-10 17:20:37 +00:00
5 changed files with 81 additions and 399 deletions

View File

@@ -260,6 +260,15 @@ export type ClusterAssignmentInput = {
userId?: InputMaybe<Scalars["Int"]["input"]>; userId?: InputMaybe<Scalars["Int"]["input"]>;
}; };
export type CohortStatsGql = {
__typename?: "CohortStatsGQL";
activated7d: Scalars["Int"]["output"];
activatedNotPaid7d: Scalars["Int"]["output"];
paid7d: Scalars["Int"]["output"];
signups: Scalars["Int"]["output"];
weekStart: Scalars["Date"]["output"];
};
export type CommentGql = { export type CommentGql = {
__typename?: "CommentGQL"; __typename?: "CommentGQL";
id: Scalars["Int"]["output"]; id: Scalars["Int"]["output"];
@@ -2400,7 +2409,7 @@ export type HomographyInfoGql = {
crop: BoundingBoxGql; crop: BoundingBoxGql;
destPoints: PocketPointsGql; destPoints: PocketPointsGql;
frameIndex: Scalars["Int"]["output"]; frameIndex: Scalars["Int"]["output"];
id?: Maybe<Scalars["Int"]["output"]>; id: Scalars["Int"]["output"];
pockets: Array<BoundingBoxGql>; pockets: Array<BoundingBoxGql>;
sourcePoints: PocketPointsGql; sourcePoints: PocketPointsGql;
}; };
@@ -3080,7 +3089,6 @@ export type Query = {
getResolvedTier: ResolvedTierGql; getResolvedTier: ResolvedTierGql;
getRuns: GetRunsResult; getRuns: GetRunsResult;
getShotAnnotationTypes: Array<ShotAnnotationTypeGql>; getShotAnnotationTypes: Array<ShotAnnotationTypeGql>;
getShotTableState: ShotTableStateGql;
getShots: Array<ShotGql>; getShots: Array<ShotGql>;
getShotsByIds: Array<ShotGql>; getShotsByIds: Array<ShotGql>;
getShotsWithMetadata: GetShotsResult; getShotsWithMetadata: GetShotsResult;
@@ -3226,10 +3234,6 @@ export type QueryGetShotAnnotationTypesArgs = {
errorTypes?: InputMaybe<Scalars["Boolean"]["input"]>; errorTypes?: InputMaybe<Scalars["Boolean"]["input"]>;
}; };
export type QueryGetShotTableStateArgs = {
shotId: Scalars["Int"]["input"];
};
export type QueryGetShotsArgs = { export type QueryGetShotsArgs = {
countRespectsLimit?: Scalars["Boolean"]["input"]; countRespectsLimit?: Scalars["Boolean"]["input"];
filterInput: FilterInput; filterInput: FilterInput;
@@ -3609,14 +3613,6 @@ export type ShotProjectionGql = {
trajectories: Array<BallTrajectoryGql>; trajectories: Array<BallTrajectoryGql>;
}; };
export type ShotTableStateGql = {
__typename?: "ShotTableStateGQL";
frameIndex: Scalars["Int"]["output"];
shotId: Scalars["Int"]["output"];
tableState: TableStateGql;
videoId: Scalars["Int"]["output"];
};
export type ShotsOrderingComponent = export type ShotsOrderingComponent =
| { | {
difficulty: FloatOrdering; difficulty: FloatOrdering;
@@ -3993,6 +3989,7 @@ export type UsageStatsDailyGql = {
export type UsageStatsGql = { export type UsageStatsGql = {
__typename?: "UsageStatsGQL"; __typename?: "UsageStatsGQL";
cohorts: Array<CohortStatsGql>;
daily: Array<UsageStatsDailyGql>; daily: Array<UsageStatsDailyGql>;
funnel: FunnelStatsGql; funnel: FunnelStatsGql;
processingLast24h: Array<ProcessingStatusCountGql>; processingLast24h: Array<ProcessingStatusCountGql>;
@@ -6606,51 +6603,6 @@ export type ReactToVideoMutation = {
reactToVideo: boolean; reactToVideo: boolean;
}; };
export type GetRecordedStreamsQueryVariables = Exact<{
limit?: Scalars["Int"]["input"];
}>;
export type GetRecordedStreamsQuery = {
__typename?: "Query";
getFeedVideos: {
__typename?: "VideoHistoryGQL";
videos: Array<{
__typename?: "VideoGQL";
id: number;
name?: string | null;
createdAt?: any | null;
screenshotUri?: string | null;
framesPerSecond: number;
totalShots: number;
owner?: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
} | null;
}>;
};
};
export type GetRecordedStreamShotsQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
limit?: Scalars["Int"]["input"];
}>;
export type GetRecordedStreamShotsQuery = {
__typename?: "Query";
getOrderedShots: {
__typename?: "GetShotsResult";
shots: Array<{
__typename?: "ShotGQL";
id: number;
videoId: number;
startFrame: number;
endFrame: number;
}>;
};
};
export type GetRunsForHighlightsQueryVariables = Exact<{ export type GetRunsForHighlightsQueryVariables = Exact<{
filterInput: RunFilterInput; filterInput: RunFilterInput;
runIds?: InputMaybe<Array<Scalars["Int"]["input"]> | Scalars["Int"]["input"]>; runIds?: InputMaybe<Array<Scalars["Int"]["input"]> | Scalars["Int"]["input"]>;
@@ -6882,28 +6834,34 @@ export type GetLiveTableStateQuery = {
__typename?: "RGBColorGQL"; __typename?: "RGBColorGQL";
hex: string; hex: string;
} | null>; } | null>;
}; homography?: {
}; __typename?: "HomographyInfoGQL";
}; id: number;
frameIndex: number;
export type GetShotTableStateQueryVariables = Exact<{ crop: {
shotId: Scalars["Int"]["input"]; __typename?: "BoundingBoxGQL";
}>; left: number;
top: number;
export type GetShotTableStateQuery = { width: number;
__typename?: "Query"; height: number;
getShotTableState: { };
__typename?: "ShotTableStateGQL"; pockets: Array<{
shotId: number; __typename?: "BoundingBoxGQL";
videoId: number; left: number;
frameIndex: number; top: number;
tableState: { width: number;
__typename?: "TableStateGQL"; height: number;
identifierToPosition: Array<Array<number>>; }>;
identifierToColor: Array<{ sourcePoints: {
__typename?: "RGBColorGQL"; __typename?: "PocketPointsGQL";
hex: string; topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
} | null>; topSide: { __typename?: "IntPoint2D"; x: number; y: number };
topRight: { __typename?: "IntPoint2D"; x: number; y: number };
bottomLeft: { __typename?: "IntPoint2D"; x: number; y: number };
bottomSide: { __typename?: "IntPoint2D"; x: number; y: number };
bottomRight: { __typename?: "IntPoint2D"; x: number; y: number };
};
} | null;
}; };
}; };
}; };
@@ -7527,6 +7485,14 @@ export type GetUsageStatsQuery = {
retentionPrevActive: number; retentionPrevActive: number;
retentionReturned: number; retentionReturned: number;
}; };
cohorts: Array<{
__typename?: "CohortStatsGQL";
weekStart: any;
signups: number;
activated7d: number;
paid7d: number;
activatedNotPaid7d: number;
}>;
windows: Array<{ windows: Array<{
__typename?: "UsageStatsWindowGQL"; __typename?: "UsageStatsWindowGQL";
label: string; label: string;
@@ -7907,7 +7873,7 @@ export type GetStreamMonitoringDetailsQuery = {
elapsedTime?: number | null; elapsedTime?: number | null;
currentHomography?: { currentHomography?: {
__typename?: "HomographyInfoGQL"; __typename?: "HomographyInfoGQL";
id?: number | null; id: number;
frameIndex: number; frameIndex: number;
crop: { crop: {
__typename?: "BoundingBoxGQL"; __typename?: "BoundingBoxGQL";
@@ -8321,7 +8287,7 @@ export type GetVideoQuery = {
} | null; } | null;
homographyHistory: Array<{ homographyHistory: Array<{
__typename?: "HomographyInfoGQL"; __typename?: "HomographyInfoGQL";
id?: number | null; id: number;
frameIndex: number; frameIndex: number;
crop: { crop: {
__typename?: "BoundingBoxGQL"; __typename?: "BoundingBoxGQL";
@@ -8569,7 +8535,7 @@ export type FindPrerecordTableLayoutMutation = {
__typename?: "Mutation"; __typename?: "Mutation";
findPrerecordTableLayout?: { findPrerecordTableLayout?: {
__typename?: "HomographyInfoGQL"; __typename?: "HomographyInfoGQL";
id?: number | null; id: number;
frameIndex: number; frameIndex: number;
crop: { crop: {
__typename?: "BoundingBoxGQL"; __typename?: "BoundingBoxGQL";
@@ -8599,7 +8565,7 @@ export type FindPrerecordTableLayoutMutation = {
export type HomographyInfoFragment = { export type HomographyInfoFragment = {
__typename?: "HomographyInfoGQL"; __typename?: "HomographyInfoGQL";
id?: number | null; id: number;
frameIndex: number; frameIndex: number;
crop: { crop: {
__typename?: "BoundingBoxGQL"; __typename?: "BoundingBoxGQL";
@@ -14253,179 +14219,6 @@ export type ReactToVideoMutationOptions = Apollo.BaseMutationOptions<
ReactToVideoMutation, ReactToVideoMutation,
ReactToVideoMutationVariables ReactToVideoMutationVariables
>; >;
export const GetRecordedStreamsDocument = gql`
query GetRecordedStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: true, excludeVideosWithNoShots: true }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
createdAt
screenshotUri
framesPerSecond
totalShots
owner {
id
username
profileImageUri
}
}
}
}
`;
/**
* __useGetRecordedStreamsQuery__
*
* To run a query within a React component, call `useGetRecordedStreamsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRecordedStreamsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRecordedStreamsQuery({
* variables: {
* limit: // value for 'limit'
* },
* });
*/
export function useGetRecordedStreamsQuery(
baseOptions?: Apollo.QueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export function useGetRecordedStreamsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export function useGetRecordedStreamsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>(GetRecordedStreamsDocument, options);
}
export type GetRecordedStreamsQueryHookResult = ReturnType<
typeof useGetRecordedStreamsQuery
>;
export type GetRecordedStreamsLazyQueryHookResult = ReturnType<
typeof useGetRecordedStreamsLazyQuery
>;
export type GetRecordedStreamsSuspenseQueryHookResult = ReturnType<
typeof useGetRecordedStreamsSuspenseQuery
>;
export type GetRecordedStreamsQueryResult = Apollo.QueryResult<
GetRecordedStreamsQuery,
GetRecordedStreamsQueryVariables
>;
export const GetRecordedStreamShotsDocument = gql`
query GetRecordedStreamShots($videoId: Int!, $limit: Int! = 200) {
getOrderedShots(
filterInput: { videoId: [$videoId] }
shotsOrdering: { orderings: [{ startFrame: { descending: false } }] }
limit: $limit
) {
shots {
id
videoId
startFrame
endFrame
}
}
}
`;
/**
* __useGetRecordedStreamShotsQuery__
*
* To run a query within a React component, call `useGetRecordedStreamShotsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRecordedStreamShotsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRecordedStreamShotsQuery({
* variables: {
* videoId: // value for 'videoId'
* limit: // value for 'limit'
* },
* });
*/
export function useGetRecordedStreamShotsQuery(
baseOptions: Apollo.QueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export function useGetRecordedStreamShotsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export function useGetRecordedStreamShotsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>(GetRecordedStreamShotsDocument, options);
}
export type GetRecordedStreamShotsQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsQuery
>;
export type GetRecordedStreamShotsLazyQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsLazyQuery
>;
export type GetRecordedStreamShotsSuspenseQueryHookResult = ReturnType<
typeof useGetRecordedStreamShotsSuspenseQuery
>;
export type GetRecordedStreamShotsQueryResult = Apollo.QueryResult<
GetRecordedStreamShotsQuery,
GetRecordedStreamShotsQueryVariables
>;
export const GetRunsForHighlightsDocument = gql` export const GetRunsForHighlightsDocument = gql`
query GetRunsForHighlights( query GetRunsForHighlights(
$filterInput: RunFilterInput! $filterInput: RunFilterInput!
@@ -14847,9 +14640,13 @@ export const GetLiveTableStateDocument = gql`
identifierToColor { identifierToColor {
hex hex
} }
homography {
...HomographyInfo
}
} }
} }
} }
${HomographyInfoFragmentDoc}
`; `;
/** /**
@@ -14917,87 +14714,6 @@ export type GetLiveTableStateQueryResult = Apollo.QueryResult<
GetLiveTableStateQuery, GetLiveTableStateQuery,
GetLiveTableStateQueryVariables GetLiveTableStateQueryVariables
>; >;
export const GetShotTableStateDocument = gql`
query GetShotTableState($shotId: Int!) {
getShotTableState(shotId: $shotId) {
shotId
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
}
}
}
`;
/**
* __useGetShotTableStateQuery__
*
* To run a query within a React component, call `useGetShotTableStateQuery` and pass it any options that fit your needs.
* When your component renders, `useGetShotTableStateQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetShotTableStateQuery({
* variables: {
* shotId: // value for 'shotId'
* },
* });
*/
export function useGetShotTableStateQuery(
baseOptions: Apollo.QueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export function useGetShotTableStateLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export function useGetShotTableStateSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>(GetShotTableStateDocument, options);
}
export type GetShotTableStateQueryHookResult = ReturnType<
typeof useGetShotTableStateQuery
>;
export type GetShotTableStateLazyQueryHookResult = ReturnType<
typeof useGetShotTableStateLazyQuery
>;
export type GetShotTableStateSuspenseQueryHookResult = ReturnType<
typeof useGetShotTableStateSuspenseQuery
>;
export type GetShotTableStateQueryResult = Apollo.QueryResult<
GetShotTableStateQuery,
GetShotTableStateQueryVariables
>;
export const SimulateShotDocument = gql` export const SimulateShotDocument = gql`
query SimulateShot($simulationInput: SimulateShotInputGQL!) { query SimulateShot($simulationInput: SimulateShotInputGQL!) {
simulateShot(simulationInput: $simulationInput) { simulateShot(simulationInput: $simulationInput) {
@@ -16090,6 +15806,13 @@ export const GetUsageStatsDocument = gql`
retentionPrevActive retentionPrevActive
retentionReturned retentionReturned
} }
cohorts {
weekStart
signups
activated7d
paid7d
activatedNotPaid7d
}
totalUsers totalUsers
totalVideos totalVideos
totalShots totalShots

View File

@@ -1,38 +0,0 @@
# Lightweight two-step picker data for loading a recorded shot into Shot Lab.
query GetRecordedStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: true, excludeVideosWithNoShots: true }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
createdAt
screenshotUri
framesPerSecond
totalShots
owner {
id
username
profileImageUri
}
}
}
}
query GetRecordedStreamShots($videoId: Int!, $limit: Int! = 200) {
getOrderedShots(
filterInput: { videoId: [$videoId] }
shotsOrdering: { orderings: [{ startFrame: { descending: false } }] }
limit: $limit
) {
shots {
id
videoId
startFrame
endFrame
}
}
}

View File

@@ -24,19 +24,8 @@ query GetLiveTableState($videoId: Int!) {
identifierToColor { identifierToColor {
hex hex
} }
} homography {
} ...HomographyInfo
}
query GetShotTableState($shotId: Int!) {
getShotTableState(shotId: $shotId) {
shotId
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
} }
} }
} }

View File

@@ -12,6 +12,13 @@ query getUsageStats($days: Int! = 30) {
retentionPrevActive retentionPrevActive
retentionReturned retentionReturned
} }
cohorts {
weekStart
signups
activated7d
paid7d
activatedNotPaid7d
}
totalUsers totalUsers
totalVideos totalVideos
totalShots totalShots

View File

@@ -73,7 +73,6 @@ type Query {
useHomography: HomographyInputGQL = null useHomography: HomographyInputGQL = null
): TableStateGQL! ): TableStateGQL!
getLiveTableState(videoId: Int!): LiveTableStateGQL! getLiveTableState(videoId: Int!): LiveTableStateGQL!
getShotTableState(shotId: Int!): ShotTableStateGQL!
simulateShot(simulationInput: SimulateShotInputGQL!): ShotProjectionGQL! simulateShot(simulationInput: SimulateShotInputGQL!): ShotProjectionGQL!
computePotAim( computePotAim(
simulationInput: SimulateShotInputGQL! simulationInput: SimulateShotInputGQL!
@@ -610,7 +609,7 @@ type VideoTagClass {
} }
type HomographyInfoGQL { type HomographyInfoGQL {
id: Int id: Int!
frameIndex: Int! frameIndex: Int!
crop: BoundingBoxGQL! crop: BoundingBoxGQL!
pockets: [BoundingBoxGQL!]! pockets: [BoundingBoxGQL!]!
@@ -1066,13 +1065,6 @@ type LiveTableStateGQL {
tableState: TableStateGQL! tableState: TableStateGQL!
} }
type ShotTableStateGQL {
shotId: Int!
videoId: Int!
frameIndex: Int!
tableState: TableStateGQL!
}
type ShotProjectionGQL { type ShotProjectionGQL {
trajectories: [BallTrajectoryGQL!]! trajectories: [BallTrajectoryGQL!]!
events: [SimulationEventGQL!]! events: [SimulationEventGQL!]!
@@ -1183,6 +1175,7 @@ type UsageStatsGQL {
totalVideos: Int! totalVideos: Int!
totalShots: Int! totalShots: Int!
totalRuns: Int! totalRuns: Int!
cohorts: [CohortStatsGQL!]!
windows: [UsageStatsWindowGQL!]! windows: [UsageStatsWindowGQL!]!
daily: [UsageStatsDailyGQL!]! daily: [UsageStatsDailyGQL!]!
processingLast24h: [ProcessingStatusCountGQL!]! processingLast24h: [ProcessingStatusCountGQL!]!
@@ -1201,6 +1194,14 @@ type FunnelStatsGQL {
retentionReturned: Int! retentionReturned: Int!
} }
type CohortStatsGQL {
weekStart: Date!
signups: Int!
activated7d: Int!
paid7d: Int!
activatedNotPaid7d: Int!
}
type UsageStatsWindowGQL { type UsageStatsWindowGQL {
label: String! label: String!
newUsers: Int! newUsers: Int!