Compare commits

..

7 Commits

Author SHA1 Message Date
Dean Wenstrand
6f356f9535 Add freemium epoch + era conversion to funnel
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:38:20 -07:00
Dean Wenstrand
5c0ff8c31b Add freemium funnel to getUsageStats
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:48:25 -07:00
Dean Wenstrand
582c20170d Add getUsageStats admin query + operation (schema regen on shot-lab tip)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:36:47 -07:00
ef4782fc14 Select detected ball colors for Shot Lab
All checks were successful
Tests / Tests (pull_request) Successful in 33s
2026-07-10 00:12:06 -07:00
905a1ca6db Merge pull request 'Add RGBColorGQL and identifierToColor to TableStateGQL' (#290) from shot-lab-ball-colors into master 2026-07-10 03:36:33 +00:00
6f63189703 Add RGBColorGQL and identifierToColor to TableStateGQL
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Regenerated from backend: Shot Lab table-state endpoints now expose a
per-ball dominant color (RGB + hex) index-aligned with ball positions.
2026-07-09 20:35:48 -07:00
958057764c Merge pull request 'Add getLiveTableState query and live-stream operations' (#288) from live-table-state into master 2026-07-10 02:12:31 +00:00
4 changed files with 331 additions and 0 deletions

View File

@@ -2299,6 +2299,20 @@ export type FloatRangeFilter = {
lessThanInclusive?: Scalars["Boolean"]["input"]; lessThanInclusive?: Scalars["Boolean"]["input"];
}; };
export type FunnelStatsGql = {
__typename?: "FunnelStatsGQL";
activationActivated: Scalars["Int"]["output"];
activationCohort: Scalars["Int"]["output"];
eraPaying: Scalars["Int"]["output"];
eraSignups: Scalars["Int"]["output"];
freemiumEpoch: Scalars["Date"]["output"];
newPaid7d: Scalars["Int"]["output"];
newPaid30d: Scalars["Int"]["output"];
payingUsers: Scalars["Int"]["output"];
retentionPrevActive: Scalars["Int"]["output"];
retentionReturned: Scalars["Int"]["output"];
};
export type GameTypeTagMetric = { export type GameTypeTagMetric = {
__typename?: "GameTypeTagMetric"; __typename?: "GameTypeTagMetric";
madeShots: Scalars["Int"]["output"]; madeShots: Scalars["Int"]["output"];
@@ -3018,6 +3032,12 @@ export type ProcessingFailedErr = {
processing: VideoProcessingGql; processing: VideoProcessingGql;
}; };
export type ProcessingStatusCountGql = {
__typename?: "ProcessingStatusCountGQL";
count: Scalars["Int"]["output"];
status: Scalars["String"]["output"];
};
export enum ProcessingStatusEnum { export enum ProcessingStatusEnum {
Created = "CREATED", Created = "CREATED",
Failed = "FAILED", Failed = "FAILED",
@@ -3065,6 +3085,7 @@ export type Query = {
getShotsWithMetadata: GetShotsResult; getShotsWithMetadata: GetShotsResult;
getStorageStatus?: Maybe<StorageStatusGql>; getStorageStatus?: Maybe<StorageStatusGql>;
getTableState: TableStateGql; getTableState: TableStateGql;
getUsageStats: UsageStatsGql;
getUser?: Maybe<UserGql>; getUser?: Maybe<UserGql>;
getUserRelationshipsMatching: UserRelationshipsResult; getUserRelationshipsMatching: UserRelationshipsResult;
getUserSubscriptionStatus: UserSubscriptionStatusGql; getUserSubscriptionStatus: UserSubscriptionStatusGql;
@@ -3229,6 +3250,10 @@ export type QueryGetTableStateArgs = {
useHomography?: InputMaybe<HomographyInputGql>; useHomography?: InputMaybe<HomographyInputGql>;
}; };
export type QueryGetUsageStatsArgs = {
days?: Scalars["Int"]["input"];
};
export type QueryGetUserArgs = { export type QueryGetUserArgs = {
userId: Scalars["Int"]["input"]; userId: Scalars["Int"]["input"];
}; };
@@ -3338,6 +3363,14 @@ export type QuotaStatusGql = {
tierName: Scalars["String"]["output"]; tierName: Scalars["String"]["output"];
}; };
export type RgbColorGql = {
__typename?: "RGBColorGQL";
b: Scalars["Int"]["output"];
g: Scalars["Int"]["output"];
hex: Scalars["String"]["output"];
r: Scalars["Int"]["output"];
};
export enum ReactionEnum { export enum ReactionEnum {
Bullseye = "BULLSEYE", Bullseye = "BULLSEYE",
Heart = "HEART", Heart = "HEART",
@@ -3803,6 +3836,7 @@ export type SyncAppleSubscriptionResultGql = {
export type TableStateGql = { export type TableStateGql = {
__typename?: "TableStateGQL"; __typename?: "TableStateGQL";
homography?: Maybe<HomographyInfoGql>; homography?: Maybe<HomographyInfoGql>;
identifierToColor: Array<Maybe<RgbColorGql>>;
identifierToPosition: Array<Array<Scalars["Float"]["output"]>>; identifierToPosition: Array<Array<Scalars["Float"]["output"]>>;
}; };
@@ -3935,6 +3969,36 @@ export type UploadStreamGql = {
uploadsCompleted: Scalars["Int"]["output"]; uploadsCompleted: Scalars["Int"]["output"];
}; };
export type UsageStatsDailyGql = {
__typename?: "UsageStatsDailyGQL";
activeUsers: Scalars["Int"]["output"];
day: Scalars["Date"]["output"];
newUsers: Scalars["Int"]["output"];
sessions: Scalars["Int"]["output"];
shots: Scalars["Int"]["output"];
};
export type UsageStatsGql = {
__typename?: "UsageStatsGQL";
daily: Array<UsageStatsDailyGql>;
funnel: FunnelStatsGql;
processingLast24h: Array<ProcessingStatusCountGql>;
totalRuns: Scalars["Int"]["output"];
totalShots: Scalars["Int"]["output"];
totalUsers: Scalars["Int"]["output"];
totalVideos: Scalars["Int"]["output"];
windows: Array<UsageStatsWindowGql>;
};
export type UsageStatsWindowGql = {
__typename?: "UsageStatsWindowGQL";
activeUsers: Scalars["Int"]["output"];
label: Scalars["String"]["output"];
newUsers: Scalars["Int"]["output"];
sessions: Scalars["Int"]["output"];
shots: Scalars["Int"]["output"];
};
export type UserGql = { export type UserGql = {
__typename?: "UserGQL"; __typename?: "UserGQL";
activeVideoId?: Maybe<Scalars["Int"]["output"]>; activeVideoId?: Maybe<Scalars["Int"]["output"]>;
@@ -6736,6 +6800,10 @@ export type GetTableStateQuery = {
getTableState: { getTableState: {
__typename?: "TableStateGQL"; __typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>; identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{
__typename?: "RGBColorGQL";
hex: string;
} | null>;
}; };
}; };
@@ -6752,6 +6820,10 @@ export type GetLiveTableStateQuery = {
tableState: { tableState: {
__typename?: "TableStateGQL"; __typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>; identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{
__typename?: "RGBColorGQL";
hex: string;
} | null>;
homography?: { homography?: {
__typename?: "HomographyInfoGQL"; __typename?: "HomographyInfoGQL";
id: number; id: number;
@@ -7378,6 +7450,55 @@ export type DeleteTagsMutation = {
deleteTags: boolean; deleteTags: boolean;
}; };
export type GetUsageStatsQueryVariables = Exact<{
days?: Scalars["Int"]["input"];
}>;
export type GetUsageStatsQuery = {
__typename?: "Query";
getUsageStats: {
__typename?: "UsageStatsGQL";
totalUsers: number;
totalVideos: number;
totalShots: number;
totalRuns: number;
funnel: {
__typename?: "FunnelStatsGQL";
freemiumEpoch: any;
eraSignups: number;
eraPaying: number;
activationCohort: number;
activationActivated: number;
payingUsers: number;
newPaid7d: number;
newPaid30d: number;
retentionPrevActive: number;
retentionReturned: number;
};
windows: Array<{
__typename?: "UsageStatsWindowGQL";
label: string;
newUsers: number;
activeUsers: number;
sessions: number;
shots: number;
}>;
daily: Array<{
__typename?: "UsageStatsDailyGQL";
day: any;
newUsers: number;
activeUsers: number;
sessions: number;
shots: number;
}>;
processingLast24h: Array<{
__typename?: "ProcessingStatusCountGQL";
status: string;
count: number;
}>;
};
};
export type GetProfileImageUploadLinkMutationVariables = Exact<{ export type GetProfileImageUploadLinkMutationVariables = Exact<{
fileExt?: InputMaybe<Scalars["String"]["input"]>; fileExt?: InputMaybe<Scalars["String"]["input"]>;
}>; }>;
@@ -14417,6 +14538,9 @@ export const GetTableStateDocument = gql`
useHomography: $useHomography useHomography: $useHomography
) { ) {
identifierToPosition identifierToPosition
identifierToColor {
hex
}
} }
} }
`; `;
@@ -14495,6 +14619,9 @@ export const GetLiveTableStateDocument = gql`
frameIndex frameIndex
tableState { tableState {
identifierToPosition identifierToPosition
identifierToColor {
hex
}
homography { homography {
...HomographyInfo ...HomographyInfo
} }
@@ -15646,6 +15773,112 @@ export type DeleteTagsMutationOptions = Apollo.BaseMutationOptions<
DeleteTagsMutation, DeleteTagsMutation,
DeleteTagsMutationVariables DeleteTagsMutationVariables
>; >;
export const GetUsageStatsDocument = gql`
query getUsageStats($days: Int! = 30) {
getUsageStats(days: $days) {
funnel {
freemiumEpoch
eraSignups
eraPaying
activationCohort
activationActivated
payingUsers
newPaid7d
newPaid30d
retentionPrevActive
retentionReturned
}
totalUsers
totalVideos
totalShots
totalRuns
windows {
label
newUsers
activeUsers
sessions
shots
}
daily {
day
newUsers
activeUsers
sessions
shots
}
processingLast24h {
status
count
}
}
}
`;
/**
* __useGetUsageStatsQuery__
*
* To run a query within a React component, call `useGetUsageStatsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetUsageStatsQuery` 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 } = useGetUsageStatsQuery({
* variables: {
* days: // value for 'days'
* },
* });
*/
export function useGetUsageStatsQuery(
baseOptions?: Apollo.QueryHookOptions<
GetUsageStatsQuery,
GetUsageStatsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetUsageStatsQuery, GetUsageStatsQueryVariables>(
GetUsageStatsDocument,
options,
);
}
export function useGetUsageStatsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetUsageStatsQuery,
GetUsageStatsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<GetUsageStatsQuery, GetUsageStatsQueryVariables>(
GetUsageStatsDocument,
options,
);
}
export function useGetUsageStatsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetUsageStatsQuery,
GetUsageStatsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetUsageStatsQuery,
GetUsageStatsQueryVariables
>(GetUsageStatsDocument, options);
}
export type GetUsageStatsQueryHookResult = ReturnType<
typeof useGetUsageStatsQuery
>;
export type GetUsageStatsLazyQueryHookResult = ReturnType<
typeof useGetUsageStatsLazyQuery
>;
export type GetUsageStatsSuspenseQueryHookResult = ReturnType<
typeof useGetUsageStatsSuspenseQuery
>;
export type GetUsageStatsQueryResult = Apollo.QueryResult<
GetUsageStatsQuery,
GetUsageStatsQueryVariables
>;
export const GetProfileImageUploadLinkDocument = gql` export const GetProfileImageUploadLinkDocument = gql`
mutation getProfileImageUploadLink($fileExt: String = ".png") { mutation getProfileImageUploadLink($fileExt: String = ".png") {
getProfileImageUploadLink(fileExt: $fileExt) { getProfileImageUploadLink(fileExt: $fileExt) {

View File

@@ -9,6 +9,9 @@ query GetTableState(
useHomography: $useHomography useHomography: $useHomography
) { ) {
identifierToPosition identifierToPosition
identifierToColor {
hex
}
} }
} }
@@ -18,6 +21,9 @@ query GetLiveTableState($videoId: Int!) {
frameIndex frameIndex
tableState { tableState {
identifierToPosition identifierToPosition
identifierToColor {
hex
}
homography { homography {
...HomographyInfo ...HomographyInfo
} }

View File

@@ -0,0 +1,38 @@
query getUsageStats($days: Int! = 30) {
getUsageStats(days: $days) {
funnel {
freemiumEpoch
eraSignups
eraPaying
activationCohort
activationActivated
payingUsers
newPaid7d
newPaid30d
retentionPrevActive
retentionReturned
}
totalUsers
totalVideos
totalShots
totalRuns
windows {
label
newUsers
activeUsers
sessions
shots
}
daily {
day
newUsers
activeUsers
sessions
shots
}
processingLast24h {
status
count
}
}
}

View File

@@ -100,6 +100,7 @@ type Query {
countRespectsLimit: Boolean! = false countRespectsLimit: Boolean! = false
): [ShotGQL!]! ): [ShotGQL!]!
getShotsByIds(ids: [Int!]!): [ShotGQL!]! getShotsByIds(ids: [Int!]!): [ShotGQL!]!
getUsageStats(days: Int! = 30): UsageStatsGQL!
getUser(userId: Int!): UserGQL getUser(userId: Int!): UserGQL
doesUsernameExist(candidateUsername: String!): Boolean! doesUsernameExist(candidateUsername: String!): Boolean!
getLoggedInUser: UserGQL getLoggedInUser: UserGQL
@@ -1020,6 +1021,14 @@ type PlayerClusterShotGQL {
type TableStateGQL { type TableStateGQL {
identifierToPosition: [[Float!]!]! identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL homography: HomographyInfoGQL
identifierToColor: [RGBColorGQL]!
}
type RGBColorGQL {
r: Int!
g: Int!
b: Int!
hex: String!
} }
input HomographyInputGQL { input HomographyInputGQL {
@@ -1160,6 +1169,51 @@ input CreatedAfter @oneOf {
createdAt: DateTime createdAt: DateTime
} }
type UsageStatsGQL {
funnel: FunnelStatsGQL!
totalUsers: Int!
totalVideos: Int!
totalShots: Int!
totalRuns: Int!
windows: [UsageStatsWindowGQL!]!
daily: [UsageStatsDailyGQL!]!
processingLast24h: [ProcessingStatusCountGQL!]!
}
type FunnelStatsGQL {
freemiumEpoch: Date!
eraSignups: Int!
eraPaying: Int!
activationCohort: Int!
activationActivated: Int!
payingUsers: Int!
newPaid7d: Int!
newPaid30d: Int!
retentionPrevActive: Int!
retentionReturned: Int!
}
type UsageStatsWindowGQL {
label: String!
newUsers: Int!
activeUsers: Int!
sessions: Int!
shots: Int!
}
type UsageStatsDailyGQL {
day: Date!
newUsers: Int!
activeUsers: Int!
sessions: Int!
shots: Int!
}
type ProcessingStatusCountGQL {
status: String!
count: Int!
}
type UserRelationshipsResult { type UserRelationshipsResult {
inquiringUser: UserGQL! inquiringUser: UserGQL!
relationships: [UserRelationship!]! relationships: [UserRelationship!]!