@atproto/bsky 0.0.251 → 0.0.254
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/api/age-assurance/const.js +19 -19
- package/dist/api/age-assurance/const.js.map +1 -1
- package/dist/kws.d.ts +8 -0
- package/dist/kws.d.ts.map +1 -1
- package/dist/kws.js +53 -4
- package/dist/kws.js.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts +6 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/app/bsky/actor/defs.defs.js +1 -0
- package/dist/lexicons/app/bsky/actor/defs.defs.js.map +1 -1
- package/package.json +28 -23
- package/bin/migration-create.ts +0 -46
- package/buf.gen.yaml +0 -12
- package/proto/bsky.proto +0 -1907
- package/proto/courier.proto +0 -84
- package/proto/rolodex.proto +0 -116
- package/src/api/age-assurance/const.ts +0 -203
- package/src/api/age-assurance/index.ts +0 -34
- package/src/api/age-assurance/kws/age-verified.test.ts +0 -98
- package/src/api/age-assurance/kws/age-verified.ts +0 -81
- package/src/api/age-assurance/kws/const.ts +0 -33
- package/src/api/age-assurance/kws/external-payload.test.ts +0 -72
- package/src/api/age-assurance/kws/external-payload.ts +0 -149
- package/src/api/age-assurance/redirects/kws-age-verified.ts +0 -107
- package/src/api/age-assurance/stash.ts +0 -23
- package/src/api/age-assurance/types.ts +0 -10
- package/src/api/age-assurance/util.ts +0 -68
- package/src/api/age-assurance/webhooks/kws-age-verified.ts +0 -75
- package/src/api/app/bsky/actor/getProfile.ts +0 -106
- package/src/api/app/bsky/actor/getProfiles.ts +0 -99
- package/src/api/app/bsky/actor/getSuggestions.ts +0 -163
- package/src/api/app/bsky/actor/searchActors.ts +0 -171
- package/src/api/app/bsky/actor/searchActorsTypeahead.ts +0 -164
- package/src/api/app/bsky/ageassurance/begin.ts +0 -175
- package/src/api/app/bsky/ageassurance/getConfig.ts +0 -16
- package/src/api/app/bsky/ageassurance/getState.ts +0 -58
- package/src/api/app/bsky/bookmark/createBookmark.ts +0 -39
- package/src/api/app/bsky/bookmark/deleteBookmark.ts +0 -32
- package/src/api/app/bsky/bookmark/getBookmarks.ts +0 -97
- package/src/api/app/bsky/bookmark/util.ts +0 -13
- package/src/api/app/bsky/contact/dismissMatch.ts +0 -26
- package/src/api/app/bsky/contact/getMatches.ts +0 -111
- package/src/api/app/bsky/contact/getSyncStatus.ts +0 -38
- package/src/api/app/bsky/contact/importContacts.ts +0 -119
- package/src/api/app/bsky/contact/removeData.ts +0 -25
- package/src/api/app/bsky/contact/sendNotification.ts +0 -33
- package/src/api/app/bsky/contact/startPhoneVerification.ts +0 -26
- package/src/api/app/bsky/contact/util.ts +0 -92
- package/src/api/app/bsky/contact/verifyPhone.ts +0 -29
- package/src/api/app/bsky/draft/createDraft.ts +0 -45
- package/src/api/app/bsky/draft/deleteDraft.ts +0 -20
- package/src/api/app/bsky/draft/getDrafts.ts +0 -44
- package/src/api/app/bsky/draft/updateDraft.ts +0 -26
- package/src/api/app/bsky/embed/getEmbedExternalView.ts +0 -154
- package/src/api/app/bsky/feed/getActorFeeds.ts +0 -101
- package/src/api/app/bsky/feed/getActorLikes.ts +0 -128
- package/src/api/app/bsky/feed/getAuthorFeed.ts +0 -279
- package/src/api/app/bsky/feed/getFeed.ts +0 -282
- package/src/api/app/bsky/feed/getFeedGenerator.ts +0 -68
- package/src/api/app/bsky/feed/getFeedGenerators.ts +0 -86
- package/src/api/app/bsky/feed/getLikes.ts +0 -151
- package/src/api/app/bsky/feed/getListFeed.ts +0 -147
- package/src/api/app/bsky/feed/getPostThread.ts +0 -156
- package/src/api/app/bsky/feed/getPosts.ts +0 -100
- package/src/api/app/bsky/feed/getQuotes.ts +0 -122
- package/src/api/app/bsky/feed/getRepostedBy.ts +0 -121
- package/src/api/app/bsky/feed/getSuggestedFeeds.ts +0 -39
- package/src/api/app/bsky/feed/getTimeline.ts +0 -127
- package/src/api/app/bsky/feed/searchPosts.ts +0 -274
- package/src/api/app/bsky/feed/searchPostsV2.ts +0 -243
- package/src/api/app/bsky/graph/getActorStarterPacks.ts +0 -103
- package/src/api/app/bsky/graph/getBlocks.ts +0 -94
- package/src/api/app/bsky/graph/getFollowers.ts +0 -153
- package/src/api/app/bsky/graph/getFollows.ts +0 -151
- package/src/api/app/bsky/graph/getKnownFollowers.ts +0 -123
- package/src/api/app/bsky/graph/getList.ts +0 -152
- package/src/api/app/bsky/graph/getListBlocks.ts +0 -87
- package/src/api/app/bsky/graph/getListMutes.ts +0 -87
- package/src/api/app/bsky/graph/getLists.ts +0 -125
- package/src/api/app/bsky/graph/getListsWithMembership.ts +0 -146
- package/src/api/app/bsky/graph/getMutes.ts +0 -87
- package/src/api/app/bsky/graph/getRelationships.ts +0 -58
- package/src/api/app/bsky/graph/getStarterPack.ts +0 -82
- package/src/api/app/bsky/graph/getStarterPacks.ts +0 -86
- package/src/api/app/bsky/graph/getStarterPacksWithMembership.ts +0 -136
- package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +0 -171
- package/src/api/app/bsky/graph/muteActor.ts +0 -24
- package/src/api/app/bsky/graph/muteActorList.ts +0 -19
- package/src/api/app/bsky/graph/muteThread.ts +0 -19
- package/src/api/app/bsky/graph/searchStarterPacks.ts +0 -169
- package/src/api/app/bsky/graph/unmuteActor.ts +0 -21
- package/src/api/app/bsky/graph/unmuteActorList.ts +0 -19
- package/src/api/app/bsky/graph/unmuteThread.ts +0 -19
- package/src/api/app/bsky/labeler/getServices.ts +0 -41
- package/src/api/app/bsky/notification/getPreferences.ts +0 -49
- package/src/api/app/bsky/notification/getUnreadCount.ts +0 -83
- package/src/api/app/bsky/notification/listActivitySubscriptions.ts +0 -116
- package/src/api/app/bsky/notification/listNotifications.ts +0 -272
- package/src/api/app/bsky/notification/putActivitySubscription.ts +0 -69
- package/src/api/app/bsky/notification/putPreferences.ts +0 -17
- package/src/api/app/bsky/notification/putPreferencesV2.ts +0 -64
- package/src/api/app/bsky/notification/registerPush.ts +0 -40
- package/src/api/app/bsky/notification/unregisterPush.ts +0 -39
- package/src/api/app/bsky/notification/updateSeen.ts +0 -52
- package/src/api/app/bsky/notification/util.ts +0 -134
- package/src/api/app/bsky/unspecced/getAgeAssuranceState.ts +0 -48
- package/src/api/app/bsky/unspecced/getConfig.ts +0 -20
- package/src/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts +0 -162
- package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +0 -91
- package/src/api/app/bsky/unspecced/getPostThreadOtherV2.ts +0 -126
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +0 -148
- package/src/api/app/bsky/unspecced/getSuggestedFeeds.ts +0 -100
- package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +0 -143
- package/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts +0 -165
- package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +0 -183
- package/src/api/app/bsky/unspecced/getSuggestedUsersForDiscover.ts +0 -175
- package/src/api/app/bsky/unspecced/getSuggestedUsersForExplore.ts +0 -178
- package/src/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.ts +0 -178
- package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +0 -27
- package/src/api/app/bsky/unspecced/getTrendingTopics.ts +0 -96
- package/src/api/app/bsky/unspecced/getTrends.ts +0 -162
- package/src/api/app/bsky/unspecced/initAgeAssurance.ts +0 -150
- package/src/api/blob-dispatcher.ts +0 -41
- package/src/api/blob-resolver.ts +0 -401
- package/src/api/com/atproto/admin/getAccountInfos.ts +0 -49
- package/src/api/com/atproto/admin/getSubjectStatus.ts +0 -79
- package/src/api/com/atproto/admin/updateSubjectStatus.ts +0 -80
- package/src/api/com/atproto/identity/resolveHandle.ts +0 -19
- package/src/api/com/atproto/label/queryLabels.ts +0 -42
- package/src/api/com/atproto/repo/getRecord.ts +0 -44
- package/src/api/com/atproto/temp/fetchLabels.ts +0 -9
- package/src/api/external.ts +0 -15
- package/src/api/health.ts +0 -47
- package/src/api/index.ts +0 -216
- package/src/api/internal/bsky/actor/getProfiles.ts +0 -87
- package/src/api/kws/api.ts +0 -113
- package/src/api/kws/index.ts +0 -29
- package/src/api/kws/types.ts +0 -78
- package/src/api/kws/util.test.ts +0 -50
- package/src/api/kws/util.ts +0 -109
- package/src/api/kws/webhook.ts +0 -134
- package/src/api/sitemap.ts +0 -76
- package/src/api/util.ts +0 -45
- package/src/api/well-known.ts +0 -44
- package/src/auth-verifier.ts +0 -522
- package/src/bsync.ts +0 -41
- package/src/cache/read-through.ts +0 -157
- package/src/config.ts +0 -719
- package/src/context.ts +0 -143
- package/src/courier.ts +0 -41
- package/src/data-plane/client/hosts.ts +0 -103
- package/src/data-plane/client/index.ts +0 -125
- package/src/data-plane/client/util.test.ts +0 -29
- package/src/data-plane/client/util.ts +0 -74
- package/src/data-plane/index.ts +0 -2
- package/src/data-plane/server/background.ts +0 -66
- package/src/data-plane/server/bsync-subscription.ts +0 -590
- package/src/data-plane/server/db/database-schema.ts +0 -90
- package/src/data-plane/server/db/db.ts +0 -204
- package/src/data-plane/server/db/index.ts +0 -2
- package/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts +0 -305
- package/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts +0 -25
- package/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts +0 -127
- package/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts +0 -14
- package/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts +0 -14
- package/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts +0 -23
- package/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts +0 -34
- package/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts +0 -85
- package/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts +0 -78
- package/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts +0 -15
- package/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts +0 -33
- package/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts +0 -27
- package/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts +0 -13
- package/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts +0 -13
- package/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts +0 -9
- package/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts +0 -15
- package/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts +0 -32
- package/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts +0 -31
- package/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts +0 -19
- package/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts +0 -14
- package/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts +0 -12
- package/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts +0 -23
- package/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts +0 -16
- package/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts +0 -13
- package/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts +0 -24
- package/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts +0 -27
- package/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts +0 -9
- package/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +0 -40
- package/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +0 -123
- package/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts +0 -66
- package/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts +0 -15
- package/src/data-plane/server/db/migrations/20240226T225725627Z-labelers.ts +0 -27
- package/src/data-plane/server/db/migrations/20240530T170337073Z-account-deactivation.ts +0 -12
- package/src/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.ts +0 -15
- package/src/data-plane/server/db/migrations/20240606T222548219Z-starter-packs.ts +0 -45
- package/src/data-plane/server/db/migrations/20240719T203853939Z-priority-notifs.ts +0 -25
- package/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts +0 -12
- package/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts +0 -28
- package/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts +0 -17
- package/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts +0 -25
- package/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts +0 -23
- package/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts +0 -17
- package/src/data-plane/server/db/migrations/20241114T153108102Z-add-starter-packs-name.ts +0 -19
- package/src/data-plane/server/db/migrations/20250116T222618297Z-post-embed-video.ts +0 -17
- package/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts +0 -9
- package/src/data-plane/server/db/migrations/20250404T163421487Z-verifications.ts +0 -39
- package/src/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.ts +0 -17
- package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +0 -9
- package/src/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.ts +0 -22
- package/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts +0 -22
- package/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts +0 -22
- package/src/data-plane/server/db/migrations/20250812T183735692Z-add-bookmarks.ts +0 -23
- package/src/data-plane/server/db/migrations/20250813T174955711Z-add-post-agg-bookmarks.ts +0 -12
- package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +0 -28
- package/src/data-plane/server/db/migrations/20260112T133951271Z-add-drafts.ts +0 -24
- package/src/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.ts +0 -19
- package/src/data-plane/server/db/migrations/index.ts +0 -60
- package/src/data-plane/server/db/migrations/provider.ts +0 -26
- package/src/data-plane/server/db/pagination.ts +0 -341
- package/src/data-plane/server/db/tables/activity-subscription.ts +0 -12
- package/src/data-plane/server/db/tables/actor-block.ts +0 -14
- package/src/data-plane/server/db/tables/actor-state.ts +0 -10
- package/src/data-plane/server/db/tables/actor-sync.ts +0 -9
- package/src/data-plane/server/db/tables/actor.ts +0 -19
- package/src/data-plane/server/db/tables/algo.ts +0 -11
- package/src/data-plane/server/db/tables/blob-takedown.ts +0 -9
- package/src/data-plane/server/db/tables/bookmark.ts +0 -11
- package/src/data-plane/server/db/tables/did-cache.ts +0 -13
- package/src/data-plane/server/db/tables/draft.ts +0 -11
- package/src/data-plane/server/db/tables/duplicate-record.ts +0 -12
- package/src/data-plane/server/db/tables/feed-generator.ts +0 -21
- package/src/data-plane/server/db/tables/feed-item.ts +0 -12
- package/src/data-plane/server/db/tables/follow.ts +0 -15
- package/src/data-plane/server/db/tables/label.ts +0 -13
- package/src/data-plane/server/db/tables/labeler.ts +0 -16
- package/src/data-plane/server/db/tables/like.ts +0 -18
- package/src/data-plane/server/db/tables/list-block.ts +0 -15
- package/src/data-plane/server/db/tables/list-item.ts +0 -16
- package/src/data-plane/server/db/tables/list-mute.ts +0 -9
- package/src/data-plane/server/db/tables/list.ts +0 -19
- package/src/data-plane/server/db/tables/mute.ts +0 -9
- package/src/data-plane/server/db/tables/notification-push-token.ts +0 -10
- package/src/data-plane/server/db/tables/notification.ts +0 -16
- package/src/data-plane/server/db/tables/post-agg.ts +0 -16
- package/src/data-plane/server/db/tables/post-embed.ts +0 -47
- package/src/data-plane/server/db/tables/post-gate.ts +0 -12
- package/src/data-plane/server/db/tables/post.ts +0 -28
- package/src/data-plane/server/db/tables/private-data.ts +0 -13
- package/src/data-plane/server/db/tables/profile-agg.ts +0 -14
- package/src/data-plane/server/db/tables/profile.ts +0 -17
- package/src/data-plane/server/db/tables/quote.ts +0 -15
- package/src/data-plane/server/db/tables/record.ts +0 -15
- package/src/data-plane/server/db/tables/repost.ts +0 -18
- package/src/data-plane/server/db/tables/starter-pack.ts +0 -17
- package/src/data-plane/server/db/tables/subscription.ts +0 -9
- package/src/data-plane/server/db/tables/suggested-feed.ts +0 -10
- package/src/data-plane/server/db/tables/suggested-follow.ts +0 -10
- package/src/data-plane/server/db/tables/tagged-suggestion.ts +0 -11
- package/src/data-plane/server/db/tables/thread-gate.ts +0 -12
- package/src/data-plane/server/db/tables/thread-mute.ts +0 -9
- package/src/data-plane/server/db/tables/verification.ts +0 -20
- package/src/data-plane/server/db/tables/view-param.ts +0 -12
- package/src/data-plane/server/db/types.ts +0 -12
- package/src/data-plane/server/db/util.ts +0 -67
- package/src/data-plane/server/index.ts +0 -44
- package/src/data-plane/server/indexing/index.ts +0 -458
- package/src/data-plane/server/indexing/plugins/block.ts +0 -84
- package/src/data-plane/server/indexing/plugins/chat-declaration.ts +0 -60
- package/src/data-plane/server/indexing/plugins/feed-generator.ts +0 -80
- package/src/data-plane/server/indexing/plugins/follow.ts +0 -132
- package/src/data-plane/server/indexing/plugins/germ-declaration.ts +0 -57
- package/src/data-plane/server/indexing/plugins/labeler.ts +0 -74
- package/src/data-plane/server/indexing/plugins/like.ts +0 -151
- package/src/data-plane/server/indexing/plugins/list-block.ts +0 -84
- package/src/data-plane/server/indexing/plugins/list-item.ts +0 -92
- package/src/data-plane/server/indexing/plugins/list.ts +0 -80
- package/src/data-plane/server/indexing/plugins/notif-declaration.ts +0 -57
- package/src/data-plane/server/indexing/plugins/post-gate.ts +0 -101
- package/src/data-plane/server/indexing/plugins/post.ts +0 -675
- package/src/data-plane/server/indexing/plugins/profile.ts +0 -90
- package/src/data-plane/server/indexing/plugins/repost.ts +0 -176
- package/src/data-plane/server/indexing/plugins/starter-pack.ts +0 -74
- package/src/data-plane/server/indexing/plugins/status.ts +0 -57
- package/src/data-plane/server/indexing/plugins/thread-gate.ts +0 -101
- package/src/data-plane/server/indexing/plugins/verification.ts +0 -113
- package/src/data-plane/server/indexing/processor.ts +0 -292
- package/src/data-plane/server/routes/activity-subscription.ts +0 -84
- package/src/data-plane/server/routes/blocks.ts +0 -127
- package/src/data-plane/server/routes/bookmarks.ts +0 -83
- package/src/data-plane/server/routes/drafts.ts +0 -37
- package/src/data-plane/server/routes/feed-gens.ts +0 -89
- package/src/data-plane/server/routes/feeds.ts +0 -176
- package/src/data-plane/server/routes/follows.ts +0 -141
- package/src/data-plane/server/routes/identity.ts +0 -59
- package/src/data-plane/server/routes/index.ts +0 -67
- package/src/data-plane/server/routes/interactions.ts +0 -120
- package/src/data-plane/server/routes/labels.ts +0 -55
- package/src/data-plane/server/routes/likes.ts +0 -92
- package/src/data-plane/server/routes/lists.ts +0 -88
- package/src/data-plane/server/routes/moderation.ts +0 -102
- package/src/data-plane/server/routes/mutes.ts +0 -195
- package/src/data-plane/server/routes/notifs.ts +0 -239
- package/src/data-plane/server/routes/profile.ts +0 -220
- package/src/data-plane/server/routes/quotes.ts +0 -32
- package/src/data-plane/server/routes/records.ts +0 -119
- package/src/data-plane/server/routes/relationships.ts +0 -174
- package/src/data-plane/server/routes/reposts.ts +0 -76
- package/src/data-plane/server/routes/search.ts +0 -181
- package/src/data-plane/server/routes/site-standard.ts +0 -12
- package/src/data-plane/server/routes/sitemap.ts +0 -43
- package/src/data-plane/server/routes/starter-packs.ts +0 -32
- package/src/data-plane/server/routes/suggestions.ts +0 -175
- package/src/data-plane/server/routes/sync.ts +0 -16
- package/src/data-plane/server/routes/threads.ts +0 -33
- package/src/data-plane/server/subscription.ts +0 -152
- package/src/data-plane/server/util.ts +0 -202
- package/src/error.ts +0 -12
- package/src/etcd.ts +0 -90
- package/src/feature-gates/README.md +0 -83
- package/src/feature-gates/gates.ts +0 -25
- package/src/feature-gates/index.ts +0 -235
- package/src/feature-gates/metrics.test.ts +0 -196
- package/src/feature-gates/metrics.ts +0 -107
- package/src/feature-gates/types.ts +0 -62
- package/src/feature-gates/utils.test.ts +0 -66
- package/src/feature-gates/utils.ts +0 -135
- package/src/hydration/actor.ts +0 -578
- package/src/hydration/external.test.ts +0 -281
- package/src/hydration/external.ts +0 -268
- package/src/hydration/feed.ts +0 -455
- package/src/hydration/graph.ts +0 -354
- package/src/hydration/hydrator.ts +0 -1835
- package/src/hydration/label.ts +0 -207
- package/src/hydration/util.ts +0 -199
- package/src/image/index.ts +0 -2
- package/src/image/invalidator.ts +0 -33
- package/src/image/logger.ts +0 -6
- package/src/image/server.ts +0 -228
- package/src/image/sharp.ts +0 -89
- package/src/image/uri.ts +0 -114
- package/src/image/util.ts +0 -41
- package/src/index.ts +0 -312
- package/src/kws.ts +0 -197
- package/src/logger.ts +0 -40
- package/src/pipeline.ts +0 -60
- package/src/redis.ts +0 -205
- package/src/rolodex.ts +0 -42
- package/src/stash.ts +0 -76
- package/src/util/debug.ts +0 -10
- package/src/util/http.ts +0 -42
- package/src/util/retry.ts +0 -10
- package/src/util/standard-site.test.ts +0 -423
- package/src/util/standard-site.ts +0 -196
- package/src/util/uris.ts +0 -38
- package/src/util.ts +0 -48
- package/src/views/index.ts +0 -2783
- package/src/views/threads-v2.ts +0 -510
- package/src/views/types.ts +0 -193
- package/src/views/util.ts +0 -114
- package/test.env +0 -2
- package/tests/__snapshots__/feed-generation.test.ts.snap +0 -2510
- package/tests/_util.ts +0 -292
- package/tests/admin/admin-auth.test.ts +0 -190
- package/tests/admin/moderation.test.ts +0 -222
- package/tests/auth.test.ts +0 -90
- package/tests/blob-resolver.test.ts +0 -125
- package/tests/data-plane/__snapshots__/indexing.test.ts.snap +0 -887
- package/tests/data-plane/db.test.ts +0 -189
- package/tests/data-plane/duplicate-records.test.ts +0 -156
- package/tests/data-plane/handle-invalidation.test.ts +0 -153
- package/tests/data-plane/indexing.test.ts +0 -827
- package/tests/data-plane/subscription.test.ts +0 -128
- package/tests/data-plane/thread-mutes.test.ts +0 -175
- package/tests/entryway-auth.test.ts +0 -177
- package/tests/etcd.test.ts +0 -302
- package/tests/feed-generation.test.ts +0 -965
- package/tests/hydration/util.test.ts +0 -119
- package/tests/image/server.test.ts +0 -97
- package/tests/image/sharp.test.ts +0 -199
- package/tests/image/uri.test.ts +0 -106
- package/tests/label-hydration.test.ts +0 -245
- package/tests/postgates.test.ts +0 -182
- package/tests/query-labels.test.ts +0 -85
- package/tests/redis-cache.test.ts +0 -230
- package/tests/seed/feed-hidden-replies.ts +0 -63
- package/tests/seed/get-suggested-starter-packs.ts +0 -64
- package/tests/seed/get-trends.ts +0 -71
- package/tests/seed/known-followers.ts +0 -111
- package/tests/seed/postgates.ts +0 -57
- package/tests/server.test.ts +0 -115
- package/tests/sitemap.test.ts +0 -75
- package/tests/stash.test.ts +0 -180
- package/tests/utils.test.ts +0 -36
- package/tests/views/__snapshots__/actor-search.test.ts.snap +0 -155
- package/tests/views/__snapshots__/author-feed.test.ts.snap +0 -6301
- package/tests/views/__snapshots__/block-lists.test.ts.snap +0 -690
- package/tests/views/__snapshots__/blocks.test.ts.snap +0 -462
- package/tests/views/__snapshots__/bookmarks.test.ts.snap +0 -379
- package/tests/views/__snapshots__/follows.test.ts.snap +0 -798
- package/tests/views/__snapshots__/labeler-service.test.ts.snap +0 -233
- package/tests/views/__snapshots__/likes.test.ts.snap +0 -124
- package/tests/views/__snapshots__/list-feed.test.ts.snap +0 -883
- package/tests/views/__snapshots__/lists.test.ts.snap +0 -650
- package/tests/views/__snapshots__/mute-lists.test.ts.snap +0 -705
- package/tests/views/__snapshots__/mutes.test.ts.snap +0 -353
- package/tests/views/__snapshots__/notifications.test.ts.snap +0 -3074
- package/tests/views/__snapshots__/posts.test.ts.snap +0 -1019
- package/tests/views/__snapshots__/profile.test.ts.snap +0 -811
- package/tests/views/__snapshots__/quotes.test.ts.snap +0 -445
- package/tests/views/__snapshots__/reposts.test.ts.snap +0 -112
- package/tests/views/__snapshots__/starter-packs.test.ts.snap +0 -836
- package/tests/views/__snapshots__/thread-v2.test.ts.snap +0 -1298
- package/tests/views/__snapshots__/thread.test.ts.snap +0 -2051
- package/tests/views/__snapshots__/threadgating.test.ts.snap +0 -189
- package/tests/views/__snapshots__/timeline.test.ts.snap +0 -7252
- package/tests/views/account-deactivation.test.ts +0 -81
- package/tests/views/actor-likes.test.ts +0 -138
- package/tests/views/actor-search.test.ts +0 -261
- package/tests/views/age-assurance-v2.test.ts +0 -823
- package/tests/views/age-assurance.test.ts +0 -479
- package/tests/views/author-feed.test.ts +0 -694
- package/tests/views/block-lists.test.ts +0 -661
- package/tests/views/blocks.test.ts +0 -889
- package/tests/views/bookmarks.test.ts +0 -345
- package/tests/views/drafts.test.ts +0 -430
- package/tests/views/feed-hidden-replies.test.ts +0 -243
- package/tests/views/feed-view-post.test.ts +0 -501
- package/tests/views/follows.test.ts +0 -372
- package/tests/views/get-config.test.ts +0 -64
- package/tests/views/get-suggested-onboarding-users.test.ts +0 -193
- package/tests/views/get-suggested-starter-packs.test.ts +0 -137
- package/tests/views/get-trends.test.ts +0 -141
- package/tests/views/internal-actor.test.ts +0 -127
- package/tests/views/known-followers.test.ts +0 -193
- package/tests/views/labeler-service.test.ts +0 -242
- package/tests/views/labels-needs-review.test.ts +0 -316
- package/tests/views/labels-takedown.test.ts +0 -247
- package/tests/views/likes.test.ts +0 -192
- package/tests/views/list-feed.test.ts +0 -195
- package/tests/views/lists.test.ts +0 -412
- package/tests/views/mute-lists.test.ts +0 -509
- package/tests/views/mutes.test.ts +0 -308
- package/tests/views/notifications.test.ts +0 -1581
- package/tests/views/post-search.test.ts +0 -281
- package/tests/views/posts-debug.test.ts +0 -79
- package/tests/views/posts.test.ts +0 -427
- package/tests/views/profile-debug.test.ts +0 -75
- package/tests/views/profile.test.ts +0 -795
- package/tests/views/quotes.test.ts +0 -133
- package/tests/views/reposts.test.ts +0 -112
- package/tests/views/starter-packs.test.ts +0 -395
- package/tests/views/suggestions.test.ts +0 -145
- package/tests/views/thread-v2.test.ts +0 -2005
- package/tests/views/thread.test.ts +0 -637
- package/tests/views/threadgating.test.ts +0 -843
- package/tests/views/timeline.test.ts +0 -328
- package/tests/views/verification.test.ts +0 -296
- package/tsconfig.build.json +0 -10
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
- package/vitest.config.ts +0 -10
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import { GrowthBookClient } from '@growthbook/growthbook'
|
|
2
|
-
import type express from 'express'
|
|
3
|
-
import { featureGatesLogger } from '../logger.js'
|
|
4
|
-
import { Gate, IGNORE_METRICS_FOR_GATES } from './gates.js'
|
|
5
|
-
import { MetricsClient } from './metrics.js'
|
|
6
|
-
import {
|
|
7
|
-
CheckedFeatureGatesMap,
|
|
8
|
-
ScopedFeatureGatesClient,
|
|
9
|
-
UserContext,
|
|
10
|
-
} from './types.js'
|
|
11
|
-
import {
|
|
12
|
-
extractUserContextFromGrowthbookUserContext,
|
|
13
|
-
mergeUserContexts,
|
|
14
|
-
normalizeUserContext,
|
|
15
|
-
parsedUserContextToTrackingMetadata,
|
|
16
|
-
} from './utils.js'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* We want this to be sufficiently high that we don't time out under
|
|
20
|
-
* normal conditions, but not so high that it takes too long to boot
|
|
21
|
-
* the server.
|
|
22
|
-
*/
|
|
23
|
-
const FETCH_TIMEOUT = 3e3 // 3 seconds
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* StatSig used to default to every 10s, but I think 1m is fine
|
|
27
|
-
*/
|
|
28
|
-
const REFETCH_INTERVAL = 60e3 // 1 minute
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* These need to match what the client sends
|
|
32
|
-
*/
|
|
33
|
-
const ANALYTICS_HEADER_DEVICE_ID = 'X-Bsky-Device-Id'
|
|
34
|
-
const ANALYTICS_HEADER_SESSION_ID = 'X-Bsky-Session-Id'
|
|
35
|
-
|
|
36
|
-
export { type ScopedFeatureGatesClient } from './types.js'
|
|
37
|
-
|
|
38
|
-
export class FeatureGatesClient {
|
|
39
|
-
private ready = false
|
|
40
|
-
private client: GrowthBookClient | undefined = undefined
|
|
41
|
-
private refreshInterval: NodeJS.Timeout | undefined = undefined
|
|
42
|
-
private metrics: MetricsClient
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Easy access to the `Gate` enum for consumers of this class, so they don't
|
|
46
|
-
* need to import it separately.
|
|
47
|
-
*/
|
|
48
|
-
Gate = Gate
|
|
49
|
-
|
|
50
|
-
constructor(
|
|
51
|
-
private config: {
|
|
52
|
-
growthBookApiHost?: string
|
|
53
|
-
growthBookClientKey?: string
|
|
54
|
-
eventProxyTrackingEndpoint?: string
|
|
55
|
-
},
|
|
56
|
-
) {
|
|
57
|
-
this.metrics = new MetricsClient({
|
|
58
|
-
trackingEndpoint: config.eventProxyTrackingEndpoint,
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async start() {
|
|
63
|
-
if (!this.config.growthBookApiHost || !this.config.growthBookClientKey) {
|
|
64
|
-
featureGatesLogger.info(
|
|
65
|
-
{},
|
|
66
|
-
'feature gates not configured, skipping initialization',
|
|
67
|
-
)
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
this.client = new GrowthBookClient({
|
|
73
|
-
apiHost: this.config.growthBookApiHost,
|
|
74
|
-
clientKey: this.config.growthBookClientKey,
|
|
75
|
-
onFeatureUsage: async (feature, result, userContext) => {
|
|
76
|
-
if (IGNORE_METRICS_FOR_GATES.has(feature as Gate)) return
|
|
77
|
-
|
|
78
|
-
await this.metrics.track(
|
|
79
|
-
'feature:viewed',
|
|
80
|
-
{
|
|
81
|
-
featureId: feature,
|
|
82
|
-
featureResultValue: result.value,
|
|
83
|
-
experimentId: result.experiment?.key,
|
|
84
|
-
variationId: result.experimentResult?.key,
|
|
85
|
-
},
|
|
86
|
-
parsedUserContextToTrackingMetadata(
|
|
87
|
-
extractUserContextFromGrowthbookUserContext(userContext),
|
|
88
|
-
),
|
|
89
|
-
)
|
|
90
|
-
},
|
|
91
|
-
trackingCallback: async (experiment, result, userContext) => {
|
|
92
|
-
/**
|
|
93
|
-
* Experiments are only fired in a feature gate has an Experiment
|
|
94
|
-
* attached in Growthbook. Howerver, we want to be extra sure that a
|
|
95
|
-
* misconfigured experiment doesn't result in a huge increase in events, so we
|
|
96
|
-
* protect this here.
|
|
97
|
-
*/
|
|
98
|
-
if (
|
|
99
|
-
result.featureId &&
|
|
100
|
-
IGNORE_METRICS_FOR_GATES.has(result.featureId as Gate)
|
|
101
|
-
)
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
await this.metrics.track(
|
|
105
|
-
'experiment:viewed',
|
|
106
|
-
{
|
|
107
|
-
experimentId: experiment.key,
|
|
108
|
-
variationId: result.key,
|
|
109
|
-
},
|
|
110
|
-
parsedUserContextToTrackingMetadata(
|
|
111
|
-
extractUserContextFromGrowthbookUserContext(userContext),
|
|
112
|
-
),
|
|
113
|
-
)
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
const { source, error } = await this.client.init({
|
|
118
|
-
timeout: FETCH_TIMEOUT,
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* This does not necessarily mean that the client completely failed,
|
|
123
|
-
* since it could just be that the request timed out. It may succeed
|
|
124
|
-
* after the timeout, or later during refreshes.
|
|
125
|
-
*
|
|
126
|
-
* @see https://docs.growthbook.io/lib/node#error-handling
|
|
127
|
-
*/
|
|
128
|
-
if (error) {
|
|
129
|
-
featureGatesLogger.error(
|
|
130
|
-
{ err: error, source },
|
|
131
|
-
'Client failed to initialize normally',
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Set up periodic refresh of feature definitions
|
|
137
|
-
*
|
|
138
|
-
* @see https://docs.growthbook.io/lib/node#refreshing-features
|
|
139
|
-
*/
|
|
140
|
-
this.refreshInterval = setInterval(async () => {
|
|
141
|
-
try {
|
|
142
|
-
await this.client?.refreshFeatures({
|
|
143
|
-
timeout: FETCH_TIMEOUT,
|
|
144
|
-
})
|
|
145
|
-
} catch (err) {
|
|
146
|
-
featureGatesLogger.error({ err }, 'Failed to refresh features')
|
|
147
|
-
}
|
|
148
|
-
}, REFETCH_INTERVAL)
|
|
149
|
-
|
|
150
|
-
/* Ready or not, here we come */
|
|
151
|
-
this.ready = true
|
|
152
|
-
} catch (err) {
|
|
153
|
-
featureGatesLogger.error({ err }, 'Client initialization failed')
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async destroy() {
|
|
158
|
-
if (this.ready) {
|
|
159
|
-
this.ready = false
|
|
160
|
-
if (this.refreshInterval) {
|
|
161
|
-
clearInterval(this.refreshInterval)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
await this.metrics.stop()
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async [Symbol.asyncDispose]() {
|
|
168
|
-
await this.destroy()
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Evaluate multiple feature gates for a given user, returning a map of gate
|
|
173
|
-
* ID to boolean result.
|
|
174
|
-
*/
|
|
175
|
-
private checkGates(
|
|
176
|
-
gates: Gate[],
|
|
177
|
-
userContext: UserContext,
|
|
178
|
-
): CheckedFeatureGatesMap {
|
|
179
|
-
const gb = this.client
|
|
180
|
-
const attributes = normalizeUserContext(userContext)
|
|
181
|
-
if (!gb || !this.ready) return new Map(gates.map((g) => [g, false]))
|
|
182
|
-
return new Map(gates.map((g) => [g, gb.isOn(g, { attributes })]))
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
scope(scopedUserContext: UserContext): ScopedFeatureGatesClient {
|
|
186
|
-
/*
|
|
187
|
-
* Create initial deviceId and sessionId values for the scoped client, to
|
|
188
|
-
* be used throughout this request lifecycle.
|
|
189
|
-
*/
|
|
190
|
-
const base = normalizeUserContext(scopedUserContext)
|
|
191
|
-
return {
|
|
192
|
-
Gate: this.Gate,
|
|
193
|
-
checkGates: (
|
|
194
|
-
gates: Gate[],
|
|
195
|
-
userContextOverrides?: Pick<UserContext, 'did'>,
|
|
196
|
-
) => {
|
|
197
|
-
const userContext = mergeUserContexts(base, userContextOverrides)
|
|
198
|
-
return this.checkGates(gates, userContext)
|
|
199
|
-
},
|
|
200
|
-
checkGate: (gate: Gate, userContextOverrides?: UserContext) => {
|
|
201
|
-
const userContext = mergeUserContexts(base, userContextOverrides)
|
|
202
|
-
return this.checkGates([gate], userContext).get(gate) || false
|
|
203
|
-
},
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Parse properties available in XRPC handlers to `UserContext`. The returned
|
|
209
|
-
* proeprties are used as GrowthBook `attributes` as well as the metadata
|
|
210
|
-
* payload for our analytics events. This ensures that the same user properties
|
|
211
|
-
* are used for both feature gate targeting and analytics.
|
|
212
|
-
*/
|
|
213
|
-
parseUserContextFromHandler({
|
|
214
|
-
viewer,
|
|
215
|
-
req,
|
|
216
|
-
}: {
|
|
217
|
-
/**
|
|
218
|
-
* The user's DID
|
|
219
|
-
*/
|
|
220
|
-
viewer: string | null
|
|
221
|
-
/**
|
|
222
|
-
* The express request object, used to extract analytics headers for the user context
|
|
223
|
-
*/
|
|
224
|
-
req: express.Request
|
|
225
|
-
}): UserContext {
|
|
226
|
-
const deviceId = req.header(ANALYTICS_HEADER_DEVICE_ID)
|
|
227
|
-
const sessionId = req.header(ANALYTICS_HEADER_SESSION_ID)
|
|
228
|
-
|
|
229
|
-
return normalizeUserContext({
|
|
230
|
-
did: viewer,
|
|
231
|
-
deviceId,
|
|
232
|
-
sessionId,
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { featureGatesLogger } from '../logger.js'
|
|
3
|
-
import { MetricsClient } from './metrics.js'
|
|
4
|
-
|
|
5
|
-
vi.mock('../logger', () => ({
|
|
6
|
-
featureGatesLogger: {
|
|
7
|
-
error: vi.fn(),
|
|
8
|
-
},
|
|
9
|
-
}))
|
|
10
|
-
|
|
11
|
-
type TestEvents = {
|
|
12
|
-
click: { button: string }
|
|
13
|
-
view: { screen: string }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Helper to flush promises and timers
|
|
17
|
-
const flushPromises = () => new Promise((r) => setImmediate(r))
|
|
18
|
-
|
|
19
|
-
describe('MetricsClient', () => {
|
|
20
|
-
let fetchMock: Mock
|
|
21
|
-
let fetchRequests: { body: any }[]
|
|
22
|
-
let client: MetricsClient<TestEvents>
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
vi.useFakeTimers({ toFake: ['setTimeout', 'setInterval'] })
|
|
26
|
-
fetchRequests = []
|
|
27
|
-
fetchMock = vi.fn().mockImplementation(async (_url, options) => {
|
|
28
|
-
const body = JSON.parse(options.body)
|
|
29
|
-
fetchRequests.push({ body })
|
|
30
|
-
return { ok: true, status: 200, text: async () => '' }
|
|
31
|
-
})
|
|
32
|
-
global.fetch = fetchMock
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
afterEach(async () => {
|
|
36
|
-
await client?.stop()
|
|
37
|
-
vi.useRealTimers()
|
|
38
|
-
vi.clearAllMocks()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('flushes events on interval', async () => {
|
|
42
|
-
client = new MetricsClient<TestEvents>({
|
|
43
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
44
|
-
})
|
|
45
|
-
await client.track('click', { button: 'submit' })
|
|
46
|
-
await client.track('view', { screen: 'home' })
|
|
47
|
-
|
|
48
|
-
expect(fetchRequests).toHaveLength(0)
|
|
49
|
-
|
|
50
|
-
// Advance past the 10 second interval
|
|
51
|
-
vi.advanceTimersByTime(10_000)
|
|
52
|
-
await flushPromises()
|
|
53
|
-
|
|
54
|
-
expect(fetchRequests).toHaveLength(1)
|
|
55
|
-
expect(fetchRequests[0].body.events).toHaveLength(2)
|
|
56
|
-
expect(fetchRequests[0].body.events[0].event).toBe('click')
|
|
57
|
-
expect(fetchRequests[0].body.events[1].event).toBe('view')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('flushes when maxBatchSize is exceeded', async () => {
|
|
61
|
-
client = new MetricsClient<TestEvents>({
|
|
62
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
63
|
-
})
|
|
64
|
-
client.maxBatchSize = 5
|
|
65
|
-
|
|
66
|
-
// Add events up to maxBatchSize (should not flush yet)
|
|
67
|
-
for (let i = 0; i < 5; i++) {
|
|
68
|
-
await client.track('click', { button: `btn-${i}` })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
expect(fetchRequests).toHaveLength(0)
|
|
72
|
-
|
|
73
|
-
// One more event should trigger flush (> maxBatchSize)
|
|
74
|
-
await client.track('click', { button: 'btn-trigger' })
|
|
75
|
-
await flushPromises()
|
|
76
|
-
|
|
77
|
-
expect(fetchRequests).toHaveLength(1)
|
|
78
|
-
expect(fetchRequests[0].body.events).toHaveLength(6)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('logs error on failed request', async () => {
|
|
82
|
-
fetchMock.mockImplementation(async () => {
|
|
83
|
-
return {
|
|
84
|
-
ok: false,
|
|
85
|
-
status: 500,
|
|
86
|
-
text: async () => 'Internal Server Error',
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
client = new MetricsClient<TestEvents>({
|
|
91
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
92
|
-
})
|
|
93
|
-
await client.track('click', { button: 'submit' })
|
|
94
|
-
|
|
95
|
-
// Trigger flush via interval
|
|
96
|
-
vi.advanceTimersByTime(10_000)
|
|
97
|
-
await flushPromises()
|
|
98
|
-
|
|
99
|
-
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
100
|
-
expect(featureGatesLogger.error).toHaveBeenCalledWith(
|
|
101
|
-
expect.objectContaining({
|
|
102
|
-
err: expect.any(Error),
|
|
103
|
-
}),
|
|
104
|
-
'Failed to send metrics',
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('handles fetch text() error gracefully', async () => {
|
|
109
|
-
fetchMock.mockImplementation(async () => {
|
|
110
|
-
return {
|
|
111
|
-
ok: false,
|
|
112
|
-
status: 500,
|
|
113
|
-
text: async () => {
|
|
114
|
-
throw new Error('Failed to read response')
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
client = new MetricsClient<TestEvents>({
|
|
120
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
121
|
-
})
|
|
122
|
-
await client.track('click', { button: 'submit' })
|
|
123
|
-
|
|
124
|
-
// Trigger flush - should not throw
|
|
125
|
-
vi.advanceTimersByTime(10_000)
|
|
126
|
-
await flushPromises()
|
|
127
|
-
|
|
128
|
-
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
129
|
-
expect(featureGatesLogger.error).toHaveBeenCalledWith(
|
|
130
|
-
expect.objectContaining({
|
|
131
|
-
err: expect.objectContaining({
|
|
132
|
-
message: expect.stringContaining('Unknown error'),
|
|
133
|
-
}),
|
|
134
|
-
}),
|
|
135
|
-
'Failed to send metrics',
|
|
136
|
-
)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('flushes when stop() is called', async () => {
|
|
140
|
-
client = new MetricsClient<TestEvents>({
|
|
141
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
142
|
-
})
|
|
143
|
-
await client.track('click', { button: 'submit' })
|
|
144
|
-
|
|
145
|
-
expect(fetchRequests).toHaveLength(0)
|
|
146
|
-
|
|
147
|
-
// Stop should flush remaining events
|
|
148
|
-
await client.stop()
|
|
149
|
-
await flushPromises()
|
|
150
|
-
|
|
151
|
-
expect(fetchRequests).toHaveLength(1)
|
|
152
|
-
expect(fetchRequests[0].body.events).toHaveLength(1)
|
|
153
|
-
expect(fetchRequests[0].body.events[0].event).toBe('click')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('does not send if trackingEndpoint is not configured', async () => {
|
|
157
|
-
client = new MetricsClient<TestEvents>({})
|
|
158
|
-
await client.track('click', { button: 'submit' })
|
|
159
|
-
|
|
160
|
-
// Trigger flush via interval
|
|
161
|
-
vi.advanceTimersByTime(10_000)
|
|
162
|
-
await flushPromises()
|
|
163
|
-
|
|
164
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('start() is idempotent', async () => {
|
|
168
|
-
client = new MetricsClient<TestEvents>({
|
|
169
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
// track() calls start() internally
|
|
173
|
-
await client.track('click', { button: 'submit' })
|
|
174
|
-
client.start()
|
|
175
|
-
client.start()
|
|
176
|
-
|
|
177
|
-
// Advance past interval - should only flush once
|
|
178
|
-
vi.advanceTimersByTime(10_000)
|
|
179
|
-
await flushPromises()
|
|
180
|
-
|
|
181
|
-
expect(fetchRequests).toHaveLength(1)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('does not flush if queue is empty', async () => {
|
|
185
|
-
client = new MetricsClient<TestEvents>({
|
|
186
|
-
trackingEndpoint: 'https://test.metrics.api',
|
|
187
|
-
})
|
|
188
|
-
client.start()
|
|
189
|
-
|
|
190
|
-
// Advance past interval with empty queue
|
|
191
|
-
vi.advanceTimersByTime(10_000)
|
|
192
|
-
await flushPromises()
|
|
193
|
-
|
|
194
|
-
expect(fetchMock).not.toHaveBeenCalled()
|
|
195
|
-
})
|
|
196
|
-
})
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { featureGatesLogger } from '../logger.js'
|
|
2
|
-
|
|
3
|
-
type Events = {
|
|
4
|
-
'experiment:viewed': {
|
|
5
|
-
experimentId: string
|
|
6
|
-
variationId: string
|
|
7
|
-
}
|
|
8
|
-
'feature:viewed': {
|
|
9
|
-
featureId: string
|
|
10
|
-
featureResultValue: unknown
|
|
11
|
-
/** Only available if feature has experiment rules applied */
|
|
12
|
-
experimentId?: string
|
|
13
|
-
/** Only available if feature has experiment rules applied */
|
|
14
|
-
variationId?: string
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type Event<M extends Record<string, any>> = {
|
|
19
|
-
time: number
|
|
20
|
-
event: keyof M
|
|
21
|
-
payload: M[keyof M]
|
|
22
|
-
metadata: Record<string, any>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type Config = {
|
|
26
|
-
trackingEndpoint?: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class MetricsClient<M extends Record<string, any> = Events> {
|
|
30
|
-
maxBatchSize = 100
|
|
31
|
-
|
|
32
|
-
private started: boolean = false
|
|
33
|
-
private queue: Event<M>[] = []
|
|
34
|
-
private flushInterval: NodeJS.Timeout | null = null
|
|
35
|
-
constructor(private config: Config) {}
|
|
36
|
-
|
|
37
|
-
start() {
|
|
38
|
-
if (this.started) return
|
|
39
|
-
this.started = true
|
|
40
|
-
this.flushInterval = setInterval(() => {
|
|
41
|
-
void this.flush()
|
|
42
|
-
}, 10_000)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async stop() {
|
|
46
|
-
if (this.flushInterval) {
|
|
47
|
-
clearInterval(this.flushInterval)
|
|
48
|
-
this.flushInterval = null
|
|
49
|
-
}
|
|
50
|
-
await this.flush()
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async track<E extends keyof M>(
|
|
54
|
-
event: E,
|
|
55
|
-
payload: M[E],
|
|
56
|
-
metadata: Record<string, any> = {},
|
|
57
|
-
) {
|
|
58
|
-
await this.start()
|
|
59
|
-
|
|
60
|
-
const e = {
|
|
61
|
-
source: 'appview',
|
|
62
|
-
time: Date.now(),
|
|
63
|
-
event,
|
|
64
|
-
payload,
|
|
65
|
-
metadata,
|
|
66
|
-
}
|
|
67
|
-
this.queue.push(e)
|
|
68
|
-
|
|
69
|
-
if (this.queue.length > this.maxBatchSize) {
|
|
70
|
-
await this.flush()
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async flush() {
|
|
75
|
-
if (!this.queue.length) return
|
|
76
|
-
const events = this.queue.splice(0, this.queue.length)
|
|
77
|
-
await this.sendBatch(events)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private async sendBatch(events: Event<M>[]) {
|
|
81
|
-
if (!this.config.trackingEndpoint) return
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const res = await fetch(this.config.trackingEndpoint, {
|
|
85
|
-
method: 'POST',
|
|
86
|
-
headers: {
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
},
|
|
89
|
-
body: JSON.stringify({ events }),
|
|
90
|
-
keepalive: true,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
if (!res.ok) {
|
|
94
|
-
const errorText = await res.text().catch(() => 'Unknown error')
|
|
95
|
-
featureGatesLogger.error(
|
|
96
|
-
{ err: new Error(`${res.status} Failed to fetch - ${errorText}`) },
|
|
97
|
-
'Failed to send metrics',
|
|
98
|
-
)
|
|
99
|
-
} else {
|
|
100
|
-
// Drain response body to allow connection reuse.
|
|
101
|
-
await res.text().catch(() => {})
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
featureGatesLogger.error({ err }, 'Failed to send metrics')
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Gate } from './gates.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The user context passed to the feature gates client for evaluation and
|
|
5
|
-
* tracking purposes.
|
|
6
|
-
*/
|
|
7
|
-
export type UserContext = {
|
|
8
|
-
did?: string | null
|
|
9
|
-
deviceId?: string | null
|
|
10
|
-
sessionId?: string | null
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* User context that has been normalized to ensure all required properties are
|
|
15
|
-
* present and have fallback values. This is the format that we expect to use
|
|
16
|
-
* for all feature gate evaluations and analytics tracking.
|
|
17
|
-
*/
|
|
18
|
-
export type NormalizedUserContext = {
|
|
19
|
-
did?: string
|
|
20
|
-
deviceId: string
|
|
21
|
-
sessionId: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* This loosely matches the metadata we send from the client for analytics
|
|
26
|
-
* events. We want to make sure we have the same properties in both places so
|
|
27
|
-
* that we can correlate feature gate evaluations with analytics events.
|
|
28
|
-
*
|
|
29
|
-
* @see https://github.com/bluesky-social/social-app/blob/76109a58dc7aafccdfbd07a81cbd9925e065d1c0/src/analytics/metadata.ts
|
|
30
|
-
*/
|
|
31
|
-
export type TrackingMetadata = {
|
|
32
|
-
base: {
|
|
33
|
-
deviceId: string
|
|
34
|
-
sessionId: string
|
|
35
|
-
}
|
|
36
|
-
session: {
|
|
37
|
-
did: string | undefined
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Pre-evaluated feature gates map, the result of
|
|
43
|
-
* `ctx.FeatureGatesClient.checkGates()`
|
|
44
|
-
*/
|
|
45
|
-
export type CheckedFeatureGatesMap = Map<Gate, boolean>
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The primary interface for interacting with feature gates in the system. This
|
|
49
|
-
* client is designed to be used in the context of an XRPC handler, where we
|
|
50
|
-
* can parse the user context from the request and then create a scoped client
|
|
51
|
-
* for that request lifecycle. The client provides methods for checking multiple
|
|
52
|
-
* gates at once, as well as checking individual gates with optional user
|
|
53
|
-
* context overrides.
|
|
54
|
-
*/
|
|
55
|
-
export type ScopedFeatureGatesClient = {
|
|
56
|
-
Gate: typeof Gate
|
|
57
|
-
checkGates(
|
|
58
|
-
gates: Gate[],
|
|
59
|
-
userContextOverrides?: UserContext,
|
|
60
|
-
): CheckedFeatureGatesMap
|
|
61
|
-
checkGate(gate: Gate, userContextOverrides?: UserContext): boolean
|
|
62
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { mergeUserContexts, normalizeUserContext } from './utils.js'
|
|
3
|
-
|
|
4
|
-
describe('normalizeUserContext', () => {
|
|
5
|
-
it('defaults', () => {
|
|
6
|
-
const ctx = normalizeUserContext({})
|
|
7
|
-
expect(ctx.did).toBeUndefined()
|
|
8
|
-
expect(ctx.deviceId).toMatch(/^anon-/)
|
|
9
|
-
expect(ctx.sessionId).toMatch(/^anon-/)
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('with did', () => {
|
|
13
|
-
const ctx = normalizeUserContext({
|
|
14
|
-
did: 'did:example:123',
|
|
15
|
-
})
|
|
16
|
-
expect(ctx.did).toBe('did:example:123')
|
|
17
|
-
expect(ctx.deviceId).toBe('did:example:123')
|
|
18
|
-
expect(ctx.sessionId).toMatch(/^anon-/)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('with did and deviceId', () => {
|
|
22
|
-
const ctx = normalizeUserContext({
|
|
23
|
-
did: 'did:example:123',
|
|
24
|
-
deviceId: 'device-456',
|
|
25
|
-
})
|
|
26
|
-
expect(ctx.did).toBe('did:example:123')
|
|
27
|
-
expect(ctx.deviceId).toBe('device-456')
|
|
28
|
-
expect(ctx.sessionId).toMatch(/^anon-/)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('with only deviceId and sessionId', () => {
|
|
32
|
-
const ctx = normalizeUserContext({
|
|
33
|
-
deviceId: 'device-456',
|
|
34
|
-
sessionId: 'session-789',
|
|
35
|
-
})
|
|
36
|
-
expect(ctx.did).toBeUndefined()
|
|
37
|
-
expect(ctx.deviceId).toBe('device-456')
|
|
38
|
-
expect(ctx.sessionId).toBe('session-789')
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('mergeUserContexts', () => {
|
|
43
|
-
it('anonymous base context, override with did', () => {
|
|
44
|
-
const base = normalizeUserContext({})
|
|
45
|
-
const merged = mergeUserContexts(base, { did: 'did:example:123' })
|
|
46
|
-
expect(merged.did).toBe('did:example:123')
|
|
47
|
-
expect(merged.deviceId).toBe('did:example:123')
|
|
48
|
-
expect(merged.sessionId).toBe(base.sessionId)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('base context with did, override with different did', () => {
|
|
52
|
-
const base = normalizeUserContext({ did: 'did:example:123' })
|
|
53
|
-
const merged = mergeUserContexts(base, { did: 'did:example:456' })
|
|
54
|
-
expect(merged.did).toBe('did:example:456')
|
|
55
|
-
expect(merged.deviceId).toBe('did:example:456')
|
|
56
|
-
expect(merged.sessionId).toMatch(/^anon-/)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('base context with did, override with same did', () => {
|
|
60
|
-
const base = normalizeUserContext({ did: 'did:example:123' })
|
|
61
|
-
const merged = mergeUserContexts(base, { did: 'did:example:123' })
|
|
62
|
-
expect(merged.did).toBe('did:example:123')
|
|
63
|
-
expect(merged.deviceId).toBe('did:example:123')
|
|
64
|
-
expect(merged.sessionId).toBe(base.sessionId)
|
|
65
|
-
})
|
|
66
|
-
})
|