Compare commits

..

15 Commits

Author SHA1 Message Date
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
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
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
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
361730cace Merge pull request 'Add per-ball trajectory color to shot serialization proto' (#289) from ball-trajectory-colors into master 2026-07-10 00:07:45 +00:00
a150e1a300 Add getLiveTableState query and live-stream operations
All checks were successful
Tests / Tests (pull_request) Successful in 10s
- schema: getLiveTableState(videoId) returning LiveTableStateGQL
  (videoId, frameIndex, tableState)
- operations: GetLiveTableState in shot_simulation.gql, lightweight
  GetLiveStreams listing in live_streams.gql
- regenerated TS client outputs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 01:15:33 -07:00
63a376ed35 Merge pull request 'Add clientUploadStatus to VideoCardFields stream fragment' (#287) from dean/recording-feed-badge into master
Reviewed-on: #287
2026-07-08 17:10:32 +00:00
31f9852f4b Add clientUploadStatus to VideoCardFields stream fragment
All checks were successful
Tests / Tests (pull_request) Successful in 12s
Lets the feed/session card distinguish camera/bridge recordings
(clientUploadStatus UPLOAD_DISABLED, phone never uploads) from phone
uploads, so it can stop showing 'UPLOAD PAUSED' for recordings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:57:50 -07:00
6 changed files with 706 additions and 0 deletions

View File

@@ -260,6 +260,15 @@ export type ClusterAssignmentInput = {
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 = {
__typename?: "CommentGQL";
id: Scalars["Int"]["output"];
@@ -2299,6 +2308,20 @@ export type FloatRangeFilter = {
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 = {
__typename?: "GameTypeTagMetric";
madeShots: Scalars["Int"]["output"];
@@ -2431,6 +2454,13 @@ export type IntPoint2DInput = {
y: Scalars["Int"]["input"];
};
export type LiveTableStateGql = {
__typename?: "LiveTableStateGQL";
frameIndex: Scalars["Int"]["output"];
tableState: TableStateGql;
videoId: Scalars["Int"]["output"];
};
export type MakePercentageIntervalGql = {
__typename?: "MakePercentageIntervalGQL";
elapsedTime: Scalars["Float"]["output"];
@@ -3011,6 +3041,12 @@ export type ProcessingFailedErr = {
processing: VideoProcessingGql;
};
export type ProcessingStatusCountGql = {
__typename?: "ProcessingStatusCountGQL";
count: Scalars["Int"]["output"];
status: Scalars["String"]["output"];
};
export enum ProcessingStatusEnum {
Created = "CREATED",
Failed = "FAILED",
@@ -3041,6 +3077,7 @@ export type Query = {
getDrillRunLeaderboard: DrillRunLeaderboardGql;
getFeedVideos: VideoHistoryGql;
getGameTypeTagMetrics: Array<GameTypeTagMetric>;
getLiveTableState: LiveTableStateGql;
getLoggedInUser?: Maybe<UserGql>;
getLongestRunsLeaderboard: RunLeaderboardGql;
getMakesLeaderboard: CountLeaderboardGql;
@@ -3057,6 +3094,7 @@ export type Query = {
getShotsWithMetadata: GetShotsResult;
getStorageStatus?: Maybe<StorageStatusGql>;
getTableState: TableStateGql;
getUsageStats: UsageStatsGql;
getUser?: Maybe<UserGql>;
getUserRelationshipsMatching: UserRelationshipsResult;
getUserSubscriptionStatus: UserSubscriptionStatusGql;
@@ -3145,6 +3183,10 @@ export type QueryGetGameTypeTagMetricsArgs = {
input: GameTypeTagMetricsInput;
};
export type QueryGetLiveTableStateArgs = {
videoId: Scalars["Int"]["input"];
};
export type QueryGetLongestRunsLeaderboardArgs = {
interval?: InputMaybe<TimeInterval>;
limit?: Scalars["Int"]["input"];
@@ -3217,6 +3259,10 @@ export type QueryGetTableStateArgs = {
useHomography?: InputMaybe<HomographyInputGql>;
};
export type QueryGetUsageStatsArgs = {
days?: Scalars["Int"]["input"];
};
export type QueryGetUserArgs = {
userId: Scalars["Int"]["input"];
};
@@ -3326,6 +3372,14 @@ export type QuotaStatusGql = {
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 {
Bullseye = "BULLSEYE",
Heart = "HEART",
@@ -3791,6 +3845,7 @@ export type SyncAppleSubscriptionResultGql = {
export type TableStateGql = {
__typename?: "TableStateGQL";
homography?: Maybe<HomographyInfoGql>;
identifierToColor: Array<Maybe<RgbColorGql>>;
identifierToPosition: Array<Array<Scalars["Float"]["output"]>>;
};
@@ -3923,6 +3978,37 @@ export type UploadStreamGql = {
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";
cohorts: Array<CohortStatsGql>;
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 = {
__typename?: "UserGQL";
activeVideoId?: Maybe<Scalars["Int"]["output"]>;
@@ -4688,6 +4774,7 @@ export type GetFeedQuery = {
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
clientUploadStatus?: ClientUploadStatusEnum | null;
} | null;
tags: Array<{
__typename?: "VideoTag";
@@ -4801,6 +4888,7 @@ export type VideoCardFieldsFragment = {
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
clientUploadStatus?: ClientUploadStatusEnum | null;
} | null;
tags: Array<{
__typename?: "VideoTag";
@@ -4947,6 +5035,7 @@ export type GetVideoFeedQuery = {
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
clientUploadStatus?: ClientUploadStatusEnum | null;
} | null;
tags: Array<{
__typename?: "VideoTag";
@@ -5136,6 +5225,43 @@ export type GetMyDrillRunsQuery = {
}>;
};
export type GetLiveStreamsQueryVariables = Exact<{
limit?: Scalars["Int"]["input"];
}>;
export type GetLiveStreamsQuery = {
__typename?: "Query";
getFeedVideos: {
__typename?: "VideoHistoryGQL";
videos: Array<{
__typename?: "VideoGQL";
id: number;
name?: string | null;
startTime?: any | null;
createdAt?: any | null;
screenshotUri?: string | null;
elapsedTime?: number | null;
owner?: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
} | null;
stream?: {
__typename?: "UploadStreamGQL";
id: string;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
} | null;
}>;
pageInfo: {
__typename?: "PageInfoGQL";
hasNextPage: boolean;
endCursor?: string | null;
};
};
};
export type GetVideoMakePercentageIntervalsQueryVariables = Exact<{
videoId: Scalars["ID"]["input"];
intervalDuration: Scalars["Int"]["input"];
@@ -6684,6 +6810,59 @@ export type GetTableStateQuery = {
getTableState: {
__typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{
__typename?: "RGBColorGQL";
hex: string;
} | null>;
};
};
export type GetLiveTableStateQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
}>;
export type GetLiveTableStateQuery = {
__typename?: "Query";
getLiveTableState: {
__typename?: "LiveTableStateGQL";
videoId: number;
frameIndex: number;
tableState: {
__typename?: "TableStateGQL";
identifierToPosition: Array<Array<number>>;
identifierToColor: Array<{
__typename?: "RGBColorGQL";
hex: string;
} | null>;
homography?: {
__typename?: "HomographyInfoGQL";
id: number;
frameIndex: number;
crop: {
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
};
pockets: Array<{
__typename?: "BoundingBoxGQL";
left: number;
top: number;
width: number;
height: number;
}>;
sourcePoints: {
__typename?: "PocketPointsGQL";
topLeft: { __typename?: "IntPoint2D"; x: number; y: number };
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;
};
};
};
@@ -7281,6 +7460,63 @@ export type DeleteTagsMutation = {
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;
};
cohorts: Array<{
__typename?: "CohortStatsGQL";
weekStart: any;
signups: number;
activated7d: number;
paid7d: number;
activatedNotPaid7d: 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<{
fileExt?: InputMaybe<Scalars["String"]["input"]>;
}>;
@@ -7880,6 +8116,7 @@ export type GetVideoCardQuery = {
streamSegmentType: StreamSegmentTypeEnum;
isCompleted: boolean;
lastSegmentUploadedAt?: any | null;
clientUploadStatus?: ClientUploadStatusEnum | null;
} | null;
tags: Array<{
__typename?: "VideoTag";
@@ -8706,6 +8943,7 @@ export const VideoCardFieldsFragmentDoc = gql`
streamSegmentType
isCompleted
lastSegmentUploadedAt
clientUploadStatus
}
tableSize
pocketSize
@@ -11584,6 +11822,105 @@ export type GetMyDrillRunsQueryResult = Apollo.QueryResult<
GetMyDrillRunsQuery,
GetMyDrillRunsQueryVariables
>;
export const GetLiveStreamsDocument = gql`
query GetLiveStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: false, requireCursorCompletion: false }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
startTime
createdAt
screenshotUri
elapsedTime
owner {
id
username
profileImageUri
}
stream {
id
isCompleted
lastSegmentUploadedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
/**
* __useGetLiveStreamsQuery__
*
* To run a query within a React component, call `useGetLiveStreamsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLiveStreamsQuery` 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 } = useGetLiveStreamsQuery({
* variables: {
* limit: // value for 'limit'
* },
* });
*/
export function useGetLiveStreamsQuery(
baseOptions?: Apollo.QueryHookOptions<
GetLiveStreamsQuery,
GetLiveStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetLiveStreamsQuery, GetLiveStreamsQueryVariables>(
GetLiveStreamsDocument,
options,
);
}
export function useGetLiveStreamsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetLiveStreamsQuery,
GetLiveStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<GetLiveStreamsQuery, GetLiveStreamsQueryVariables>(
GetLiveStreamsDocument,
options,
);
}
export function useGetLiveStreamsSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetLiveStreamsQuery,
GetLiveStreamsQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetLiveStreamsQuery,
GetLiveStreamsQueryVariables
>(GetLiveStreamsDocument, options);
}
export type GetLiveStreamsQueryHookResult = ReturnType<
typeof useGetLiveStreamsQuery
>;
export type GetLiveStreamsLazyQueryHookResult = ReturnType<
typeof useGetLiveStreamsLazyQuery
>;
export type GetLiveStreamsSuspenseQueryHookResult = ReturnType<
typeof useGetLiveStreamsSuspenseQuery
>;
export type GetLiveStreamsQueryResult = Apollo.QueryResult<
GetLiveStreamsQuery,
GetLiveStreamsQueryVariables
>;
export const GetVideoMakePercentageIntervalsDocument = gql`
query GetVideoMakePercentageIntervals(
$videoId: ID!
@@ -14219,6 +14556,9 @@ export const GetTableStateDocument = gql`
useHomography: $useHomography
) {
identifierToPosition
identifierToColor {
hex
}
}
}
`;
@@ -14290,6 +14630,90 @@ export type GetTableStateQueryResult = Apollo.QueryResult<
GetTableStateQuery,
GetTableStateQueryVariables
>;
export const GetLiveTableStateDocument = gql`
query GetLiveTableState($videoId: Int!) {
getLiveTableState(videoId: $videoId) {
videoId
frameIndex
tableState {
identifierToPosition
identifierToColor {
hex
}
homography {
...HomographyInfo
}
}
}
}
${HomographyInfoFragmentDoc}
`;
/**
* __useGetLiveTableStateQuery__
*
* To run a query within a React component, call `useGetLiveTableStateQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLiveTableStateQuery` 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 } = useGetLiveTableStateQuery({
* variables: {
* videoId: // value for 'videoId'
* },
* });
*/
export function useGetLiveTableStateQuery(
baseOptions: Apollo.QueryHookOptions<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>(GetLiveTableStateDocument, options);
}
export function useGetLiveTableStateLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>(GetLiveTableStateDocument, options);
}
export function useGetLiveTableStateSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>(GetLiveTableStateDocument, options);
}
export type GetLiveTableStateQueryHookResult = ReturnType<
typeof useGetLiveTableStateQuery
>;
export type GetLiveTableStateLazyQueryHookResult = ReturnType<
typeof useGetLiveTableStateLazyQuery
>;
export type GetLiveTableStateSuspenseQueryHookResult = ReturnType<
typeof useGetLiveTableStateSuspenseQuery
>;
export type GetLiveTableStateQueryResult = Apollo.QueryResult<
GetLiveTableStateQuery,
GetLiveTableStateQueryVariables
>;
export const SimulateShotDocument = gql`
query SimulateShot($simulationInput: SimulateShotInputGQL!) {
simulateShot(simulationInput: $simulationInput) {
@@ -15367,6 +15791,119 @@ export type DeleteTagsMutationOptions = Apollo.BaseMutationOptions<
DeleteTagsMutation,
DeleteTagsMutationVariables
>;
export const GetUsageStatsDocument = gql`
query getUsageStats($days: Int! = 30) {
getUsageStats(days: $days) {
funnel {
freemiumEpoch
eraSignups
eraPaying
activationCohort
activationActivated
payingUsers
newPaid7d
newPaid30d
retentionPrevActive
retentionReturned
}
cohorts {
weekStart
signups
activated7d
paid7d
activatedNotPaid7d
}
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`
mutation getProfileImageUploadLink($fileExt: String = ".png") {
getProfileImageUploadLink(fileExt: $fileExt) {

View File

@@ -44,6 +44,7 @@ fragment VideoCardFields on VideoGQL {
streamSegmentType
isCompleted
lastSegmentUploadedAt
clientUploadStatus
}
tableSize
pocketSize

View File

@@ -0,0 +1,34 @@
# Lightweight listing of live (in-progress) streams for pickers like the
# Shot Lab "follow a live stream" flow. Selects only what a picker card
# needs; intentionally avoids the heavy VideoCardFields fragment.
query GetLiveStreams($limit: Int! = 25) {
getFeedVideos(
limit: $limit
filters: { isStreamCompleted: false, requireCursorCompletion: false }
includePrivate: MINE
feedInput: { allUsers: true }
) {
videos {
id
name
startTime
createdAt
screenshotUri
elapsedTime
owner {
id
username
profileImageUri
}
stream {
id
isCompleted
lastSegmentUploadedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}

View File

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

View File

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

View File

@@ -72,6 +72,7 @@ type Query {
tableSize: Float = 100
useHomography: HomographyInputGQL = null
): TableStateGQL!
getLiveTableState(videoId: Int!): LiveTableStateGQL!
simulateShot(simulationInput: SimulateShotInputGQL!): ShotProjectionGQL!
computePotAim(
simulationInput: SimulateShotInputGQL!
@@ -99,6 +100,7 @@ type Query {
countRespectsLimit: Boolean! = false
): [ShotGQL!]!
getShotsByIds(ids: [Int!]!): [ShotGQL!]!
getUsageStats(days: Int! = 30): UsageStatsGQL!
getUser(userId: Int!): UserGQL
doesUsernameExist(candidateUsername: String!): Boolean!
getLoggedInUser: UserGQL
@@ -1019,6 +1021,14 @@ type PlayerClusterShotGQL {
type TableStateGQL {
identifierToPosition: [[Float!]!]!
homography: HomographyInfoGQL
identifierToColor: [RGBColorGQL]!
}
type RGBColorGQL {
r: Int!
g: Int!
b: Int!
hex: String!
}
input HomographyInputGQL {
@@ -1049,6 +1059,12 @@ input IntPoint2DInput {
y: Int!
}
type LiveTableStateGQL {
videoId: Int!
frameIndex: Int!
tableState: TableStateGQL!
}
type ShotProjectionGQL {
trajectories: [BallTrajectoryGQL!]!
events: [SimulationEventGQL!]!
@@ -1153,6 +1169,60 @@ input CreatedAfter @oneOf {
createdAt: DateTime
}
type UsageStatsGQL {
funnel: FunnelStatsGQL!
totalUsers: Int!
totalVideos: Int!
totalShots: Int!
totalRuns: Int!
cohorts: [CohortStatsGQL!]!
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 CohortStatsGQL {
weekStart: Date!
signups: Int!
activated7d: Int!
paid7d: Int!
activatedNotPaid7d: 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 {
inquiringUser: UserGQL!
relationships: [UserRelationship!]!