@atproto/bsky 0.0.216 → 0.0.218
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 +21 -0
- package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/searchPosts.js +6 -4
- package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js +9 -2
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +9 -2
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
- package/dist/auth-verifier.d.ts +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +3 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/data-plane/server/db/pagination.d.ts +3 -3
- package/dist/feature-gates/gates.d.ts +5 -0
- package/dist/feature-gates/gates.d.ts.map +1 -0
- package/dist/feature-gates/gates.js +6 -0
- package/dist/feature-gates/gates.js.map +1 -0
- package/dist/feature-gates/index.d.ts +24 -0
- package/dist/feature-gates/index.d.ts.map +1 -0
- package/dist/feature-gates/index.js +135 -0
- package/dist/feature-gates/index.js.map +1 -0
- package/dist/feature-gates/metrics.d.ts +32 -0
- package/dist/feature-gates/metrics.d.ts.map +1 -0
- package/dist/feature-gates/metrics.js +100 -0
- package/dist/feature-gates/metrics.js.map +1 -0
- package/dist/feature-gates/metrics.test.d.ts +2 -0
- package/dist/feature-gates/metrics.test.d.ts.map +1 -0
- package/dist/feature-gates/metrics.test.js +152 -0
- package/dist/feature-gates/metrics.test.js.map +1 -0
- package/dist/feature-gates/types.d.ts +49 -0
- package/dist/feature-gates/types.d.ts.map +1 -0
- package/dist/feature-gates/types.js +3 -0
- package/dist/feature-gates/types.js.map +1 -0
- package/dist/feature-gates/utils.d.ts +21 -0
- package/dist/feature-gates/utils.d.ts.map +1 -0
- package/dist/feature-gates/utils.js +85 -0
- package/dist/feature-gates/utils.js.map +1 -0
- package/dist/hydration/hydrator.d.ts +8 -3
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +9 -5
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +8 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +4 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/sendInteractions.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/sendInteractions.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/sendInteractions.js.map +1 -1
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +7 -4
- package/dist/views/index.js.map +1 -1
- package/package.json +12 -12
- package/src/api/app/bsky/feed/searchPosts.ts +10 -8
- package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +0 -1
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +3 -3
- package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +13 -6
- package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +13 -6
- package/src/config.ts +8 -0
- package/src/context.ts +4 -4
- package/src/feature-gates/README.md +47 -0
- package/src/feature-gates/gates.ts +9 -0
- package/src/feature-gates/index.ts +146 -0
- package/src/feature-gates/metrics.test.ts +196 -0
- package/src/feature-gates/metrics.ts +107 -0
- package/src/feature-gates/types.ts +52 -0
- package/src/feature-gates/utils.ts +90 -0
- package/src/hydration/hydrator.ts +12 -6
- package/src/index.ts +8 -7
- package/src/lexicon/lexicons.ts +4 -0
- package/src/lexicon/types/app/bsky/feed/sendInteractions.ts +1 -0
- package/src/views/index.ts +9 -8
- package/tests/views/thread.test.ts +2 -0
- package/tests/views/verification.test.ts +19 -37
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/feature-gates.d.ts +0 -44
- package/dist/feature-gates.d.ts.map +0 -1
- package/dist/feature-gates.js +0 -133
- package/dist/feature-gates.js.map +0 -1
- package/src/feature-gates.ts +0 -136
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { featureGatesLogger } from '../logger'
|
|
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
|
+
this.flush()
|
|
42
|
+
}, 10_000)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
stop() {
|
|
46
|
+
if (this.flushInterval) {
|
|
47
|
+
clearInterval(this.flushInterval)
|
|
48
|
+
this.flushInterval = null
|
|
49
|
+
}
|
|
50
|
+
this.flush()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
track<E extends keyof M>(
|
|
54
|
+
event: E,
|
|
55
|
+
payload: M[E],
|
|
56
|
+
metadata: Record<string, any> = {},
|
|
57
|
+
) {
|
|
58
|
+
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
|
+
this.flush()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
flush() {
|
|
75
|
+
if (!this.queue.length) return
|
|
76
|
+
const events = this.queue.splice(0, this.queue.length)
|
|
77
|
+
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type express from 'express'
|
|
2
|
+
import { FeatureGate } from './gates'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The user context passed to the feature gates client for evaluation and
|
|
6
|
+
* tracking purposes.
|
|
7
|
+
*/
|
|
8
|
+
export type RawUserContext = {
|
|
9
|
+
/**
|
|
10
|
+
* The user's DID
|
|
11
|
+
*/
|
|
12
|
+
viewer: string | null
|
|
13
|
+
/**
|
|
14
|
+
* The express request object, used to extract analytics headers for the user context
|
|
15
|
+
*/
|
|
16
|
+
req: express.Request
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracted values from the `RawUserContext`. These values should match the
|
|
21
|
+
* `attributes` we've configured for GrowthBook in our GB dashboard. We also
|
|
22
|
+
* send these same values as properties in our analytics events, so we want to
|
|
23
|
+
* make sure they are consistent.
|
|
24
|
+
*/
|
|
25
|
+
export type ParsedUserContext = {
|
|
26
|
+
did?: string | null
|
|
27
|
+
deviceId: string
|
|
28
|
+
sessionId: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This loosely matches the metadata we send from the client for analytics
|
|
33
|
+
* events. We want to make sure we have the same properties in both places so
|
|
34
|
+
* that we can correlate feature gate evaluations with analytics events.
|
|
35
|
+
*
|
|
36
|
+
* @see https://github.com/bluesky-social/social-app/blob/76109a58dc7aafccdfbd07a81cbd9925e065d1c0/src/analytics/metadata.ts
|
|
37
|
+
*/
|
|
38
|
+
export type TrackingMetadata = {
|
|
39
|
+
base: {
|
|
40
|
+
deviceId: string
|
|
41
|
+
sessionId: string
|
|
42
|
+
}
|
|
43
|
+
session: {
|
|
44
|
+
did: string | undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pre-evaluated feature gates map, the result of
|
|
50
|
+
* `ctx.FeatureGatesClient.checkGates()`
|
|
51
|
+
*/
|
|
52
|
+
export type CheckedFeatureGatesMap = Map<FeatureGate, boolean>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { type UserContext as GrowthBookUserContext } from '@growthbook/growthbook'
|
|
3
|
+
import { ParsedUserContext, RawUserContext, TrackingMetadata } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* These need to match what the client sends
|
|
7
|
+
*/
|
|
8
|
+
const ANALYTICS_HEADER_DEVICE_ID = 'X-Bsky-Device-Id'
|
|
9
|
+
const ANALYTICS_HEADER_SESSION_ID = 'X-Bsky-Session-Id'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse the `RawUserContext` into a `ParsedUserContext` that is used as
|
|
13
|
+
* GrowthBook `attributes` as well as the metadata payload for our analytics
|
|
14
|
+
* events. This ensures that the same user properties are used for both feature
|
|
15
|
+
* gate targeting and analytics.
|
|
16
|
+
*/
|
|
17
|
+
export function parseRawUserContext(
|
|
18
|
+
userContext: RawUserContext,
|
|
19
|
+
): ParsedUserContext {
|
|
20
|
+
const did = userContext.viewer
|
|
21
|
+
|
|
22
|
+
// prioritize passthrough header
|
|
23
|
+
let deviceId = userContext.req.header(ANALYTICS_HEADER_DEVICE_ID)
|
|
24
|
+
if (!deviceId) {
|
|
25
|
+
if (did) {
|
|
26
|
+
/*
|
|
27
|
+
* If we don't have a device header, fall back to the DID. Our event
|
|
28
|
+
* proxy ensures ordering based on this deviceId (also called a stableId
|
|
29
|
+
* in the proxy), so if we have a DID, we want to use it to ensure client
|
|
30
|
+
* and server events are properly ordered.
|
|
31
|
+
*/
|
|
32
|
+
deviceId = did
|
|
33
|
+
} else {
|
|
34
|
+
/*
|
|
35
|
+
* Without any better option for identifying the user, we generate a
|
|
36
|
+
* random deviceId.
|
|
37
|
+
*/
|
|
38
|
+
deviceId = `anon-${crypto.randomUUID()}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// prioritize passthrough header
|
|
43
|
+
let sessionId = userContext.req.header(ANALYTICS_HEADER_SESSION_ID)
|
|
44
|
+
if (!sessionId) {
|
|
45
|
+
/*
|
|
46
|
+
* Without any better option for identifying the user, we generate a
|
|
47
|
+
* random deviceId.
|
|
48
|
+
*/
|
|
49
|
+
sessionId = `anon-${crypto.randomUUID()}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
did,
|
|
54
|
+
deviceId,
|
|
55
|
+
sessionId,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract the `ParsedUserContext` from the GrowthBook `UserContext`, which we
|
|
61
|
+
* passed into `isOn` as `attributes`.
|
|
62
|
+
*/
|
|
63
|
+
export function extractParsedUserContextFromGrowthBookUserContext(
|
|
64
|
+
userContext: GrowthBookUserContext,
|
|
65
|
+
): ParsedUserContext {
|
|
66
|
+
return {
|
|
67
|
+
did: userContext.attributes?.did,
|
|
68
|
+
deviceId: userContext.attributes?.deviceId,
|
|
69
|
+
sessionId: userContext.attributes?.sessionId,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert the `ParsedUserContext` into the `TrackingMetadata` format that we
|
|
75
|
+
* use for our analytics events. This ensures that we have the same user
|
|
76
|
+
* properties as we do for events from our client app.
|
|
77
|
+
*/
|
|
78
|
+
export function parsedUserContextToTrackingMetadata(
|
|
79
|
+
parsedUserContext: ParsedUserContext,
|
|
80
|
+
): TrackingMetadata {
|
|
81
|
+
return {
|
|
82
|
+
base: {
|
|
83
|
+
deviceId: parsedUserContext.deviceId,
|
|
84
|
+
sessionId: parsedUserContext.sessionId,
|
|
85
|
+
},
|
|
86
|
+
session: {
|
|
87
|
+
did: parsedUserContext.did ?? undefined,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -2,7 +2,7 @@ import assert from 'node:assert'
|
|
|
2
2
|
import { mapDefined } from '@atproto/common'
|
|
3
3
|
import { AtUri } from '@atproto/syntax'
|
|
4
4
|
import { DataPlaneClient } from '../data-plane/client'
|
|
5
|
-
import { type CheckedFeatureGatesMap
|
|
5
|
+
import { type CheckedFeatureGatesMap } from '../feature-gates/types'
|
|
6
6
|
import { ids } from '../lexicon/lexicons'
|
|
7
7
|
import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'
|
|
8
8
|
import { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record'
|
|
@@ -83,7 +83,13 @@ export class HydrateCtx {
|
|
|
83
83
|
overrideIncludeTakedownsForActor = this.vals.overrideIncludeTakedownsForActor
|
|
84
84
|
include3pBlocks = this.vals.include3pBlocks
|
|
85
85
|
includeDebugField = this.vals.includeDebugField
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Cache of evaluated feature gates to be used in a given request lifecycle.
|
|
88
|
+
* The actual evaluations happen at the top of the route handler and the
|
|
89
|
+
* results are stored in this map.
|
|
90
|
+
*/
|
|
91
|
+
featureGatesMap: CheckedFeatureGatesMap =
|
|
92
|
+
this.vals.featureGatesMap || new Map()
|
|
87
93
|
constructor(private vals: HydrateCtxVals) {}
|
|
88
94
|
// Convenience with use with dataplane.getActors cache control
|
|
89
95
|
get skipCacheForViewer() {
|
|
@@ -102,7 +108,7 @@ export type HydrateCtxVals = {
|
|
|
102
108
|
overrideIncludeTakedownsForActor?: boolean
|
|
103
109
|
include3pBlocks?: boolean
|
|
104
110
|
includeDebugField?: boolean
|
|
105
|
-
|
|
111
|
+
featureGatesMap?: CheckedFeatureGatesMap
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
export type HydrationState = {
|
|
@@ -746,8 +752,8 @@ export class Hydrator {
|
|
|
746
752
|
ctx: HydrateCtx,
|
|
747
753
|
): Promise<HydrationState> {
|
|
748
754
|
const postsState = await this.hydratePosts(refs, ctx, undefined, {
|
|
749
|
-
processDynamicTagsForView: ctx.
|
|
750
|
-
|
|
755
|
+
processDynamicTagsForView: ctx.featureGatesMap.get(
|
|
756
|
+
'threads:reply_ranking_exploration:enable',
|
|
751
757
|
)
|
|
752
758
|
? 'thread'
|
|
753
759
|
: undefined,
|
|
@@ -1331,7 +1337,7 @@ export class Hydrator {
|
|
|
1331
1337
|
includeTakedowns: vals.includeTakedowns,
|
|
1332
1338
|
include3pBlocks: vals.include3pBlocks,
|
|
1333
1339
|
includeDebugField,
|
|
1334
|
-
|
|
1340
|
+
featureGatesMap: vals.featureGatesMap,
|
|
1335
1341
|
})
|
|
1336
1342
|
}
|
|
1337
1343
|
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
createDataPlaneClient,
|
|
24
24
|
} from './data-plane/client'
|
|
25
25
|
import * as error from './error'
|
|
26
|
-
import {
|
|
26
|
+
import { FeatureGatesClient } from './feature-gates'
|
|
27
27
|
import { Hydrator } from './hydration/hydrator'
|
|
28
28
|
import * as imageServer from './image/server'
|
|
29
29
|
import { ImageUriBuilder } from './image/uri'
|
|
@@ -181,9 +181,10 @@ export class BskyAppView {
|
|
|
181
181
|
entrywayJwtPublicKey,
|
|
182
182
|
})
|
|
183
183
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
const featureGatesClient = new FeatureGatesClient({
|
|
185
|
+
growthBookApiHost: config.growthBookApiHost,
|
|
186
|
+
growthBookClientKey: config.growthBookClientKey,
|
|
187
|
+
eventProxyTrackingEndpoint: config.eventProxyTrackingEndpoint,
|
|
187
188
|
})
|
|
188
189
|
|
|
189
190
|
const blobDispatcher = createBlobDispatcher(config)
|
|
@@ -205,7 +206,7 @@ export class BskyAppView {
|
|
|
205
206
|
courierClient,
|
|
206
207
|
rolodexClient,
|
|
207
208
|
authVerifier,
|
|
208
|
-
|
|
209
|
+
featureGatesClient,
|
|
209
210
|
blobDispatcher,
|
|
210
211
|
kwsClient,
|
|
211
212
|
})
|
|
@@ -241,7 +242,7 @@ export class BskyAppView {
|
|
|
241
242
|
if (this.ctx.dataplaneHostList instanceof EtcdHostList) {
|
|
242
243
|
await this.ctx.dataplaneHostList.connect()
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
+
this.ctx.featureGatesClient.start() // lazy, no await
|
|
245
246
|
const server = this.app.listen(this.ctx.cfg.port)
|
|
246
247
|
this.server = server
|
|
247
248
|
server.keepAliveTimeout = 90000
|
|
@@ -253,7 +254,7 @@ export class BskyAppView {
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
async destroy(): Promise<void> {
|
|
256
|
-
this.ctx.
|
|
257
|
+
this.ctx.featureGatesClient.destroy()
|
|
257
258
|
await this.terminator?.terminate()
|
|
258
259
|
await this.ctx.etcd?.close()
|
|
259
260
|
}
|
package/src/lexicon/lexicons.ts
CHANGED
package/src/views/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { HOUR, MINUTE, mapDefined } from '@atproto/common'
|
|
2
2
|
import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
|
|
3
|
-
import { FeatureGateID } from '../feature-gates'
|
|
4
3
|
import { Actor, ProfileViewerState } from '../hydration/actor'
|
|
5
4
|
import { FeedItem, Like, Post, Repost } from '../hydration/feed'
|
|
6
5
|
import { Follow, Verification } from '../hydration/graph'
|
|
@@ -504,6 +503,10 @@ export class Views {
|
|
|
504
503
|
const actor = state.actors?.get(did)
|
|
505
504
|
if (!actor) return
|
|
506
505
|
|
|
506
|
+
// Currently, the handle comes as "handle.invalid" from the production dataplane.
|
|
507
|
+
// But the contract allows for empty handle, so we cover both cases.
|
|
508
|
+
if (!actor.handle || actor.handle === INVALID_HANDLE) return
|
|
509
|
+
|
|
507
510
|
const isImpersonation = state.labels?.get(did)?.isImpersonation
|
|
508
511
|
|
|
509
512
|
const verifications: VerificationView[] = actor.verifications.map(
|
|
@@ -1416,8 +1419,8 @@ export class Views {
|
|
|
1416
1419
|
threadTagsHide: this.threadTagsHide,
|
|
1417
1420
|
visibilityTagRankPrefix: this.visibilityTagRankPrefix,
|
|
1418
1421
|
},
|
|
1419
|
-
state.ctx?.
|
|
1420
|
-
|
|
1422
|
+
state.ctx?.featureGatesMap.get(
|
|
1423
|
+
'threads:reply_ranking_exploration:enable',
|
|
1421
1424
|
),
|
|
1422
1425
|
)
|
|
1423
1426
|
|
|
@@ -1789,8 +1792,8 @@ export class Views {
|
|
|
1789
1792
|
threadTagsHide: this.threadTagsHide,
|
|
1790
1793
|
visibilityTagRankPrefix: this.visibilityTagRankPrefix,
|
|
1791
1794
|
},
|
|
1792
|
-
state.ctx?.
|
|
1793
|
-
|
|
1795
|
+
state.ctx?.featureGatesMap.get(
|
|
1796
|
+
'threads:reply_ranking_exploration:enable',
|
|
1794
1797
|
),
|
|
1795
1798
|
)
|
|
1796
1799
|
}
|
|
@@ -1987,9 +1990,7 @@ export class Views {
|
|
|
1987
1990
|
|
|
1988
1991
|
let hiddenByTag = false
|
|
1989
1992
|
if (
|
|
1990
|
-
state.ctx?.
|
|
1991
|
-
FeatureGateID.ThreadsReplyRankingExplorationEnable,
|
|
1992
|
-
)
|
|
1993
|
+
state.ctx?.featureGatesMap.get('threads:reply_ranking_exploration:enable')
|
|
1993
1994
|
) {
|
|
1994
1995
|
hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)
|
|
1995
1996
|
} else {
|
|
@@ -17,7 +17,7 @@ describe('verification views', () => {
|
|
|
17
17
|
let network: TestNetwork
|
|
18
18
|
let agent: AtpAgent
|
|
19
19
|
let labelerDid: string
|
|
20
|
-
let sc: SeedClient
|
|
20
|
+
let sc: SeedClient<TestNetwork>
|
|
21
21
|
|
|
22
22
|
// account dids, for convenience
|
|
23
23
|
let alice: string
|
|
@@ -31,6 +31,8 @@ describe('verification views', () => {
|
|
|
31
31
|
let verifier1: string
|
|
32
32
|
let verifier2: string
|
|
33
33
|
let verifier3: string
|
|
34
|
+
let handleinvalid: string
|
|
35
|
+
let handleempty: string
|
|
34
36
|
|
|
35
37
|
beforeAll(async () => {
|
|
36
38
|
network = await TestNetwork.create({
|
|
@@ -38,24 +40,11 @@ describe('verification views', () => {
|
|
|
38
40
|
})
|
|
39
41
|
agent = network.bsky.getClient()
|
|
40
42
|
sc = network.getSeedClient()
|
|
41
|
-
await verificationsSeed(sc)
|
|
42
|
-
|
|
43
|
-
labelerDid = network.bsky.ctx.cfg.modServiceDid
|
|
44
|
-
await createLabel({
|
|
45
|
-
src: labelerDid,
|
|
46
|
-
uri: sc.dids.impersonator,
|
|
47
|
-
cid: '',
|
|
48
|
-
val: 'impersonation',
|
|
49
|
-
})
|
|
50
|
-
await createLabel({
|
|
51
|
-
src: labelerDid,
|
|
52
|
-
uri: sc.dids.verifier3,
|
|
53
|
-
cid: '',
|
|
54
|
-
val: 'impersonation',
|
|
55
|
-
})
|
|
56
43
|
|
|
44
|
+
await verificationsSeed(sc)
|
|
57
45
|
await network.processAll()
|
|
58
46
|
|
|
47
|
+
labelerDid = network.bsky.ctx.cfg.modServiceDid
|
|
59
48
|
alice = sc.dids.alice
|
|
60
49
|
bob = sc.dids.bob
|
|
61
50
|
carol = sc.dids.carol
|
|
@@ -67,6 +56,8 @@ describe('verification views', () => {
|
|
|
67
56
|
verifier1 = sc.dids.verifier1
|
|
68
57
|
verifier2 = sc.dids.verifier2
|
|
69
58
|
verifier3 = sc.dids.verifier3
|
|
59
|
+
handleinvalid = sc.dids.handleinvalid
|
|
60
|
+
handleempty = sc.dids.handleempty
|
|
70
61
|
|
|
71
62
|
await network.bsky.db.db
|
|
72
63
|
.updateTable('actor')
|
|
@@ -241,6 +232,18 @@ describe('verification views', () => {
|
|
|
241
232
|
`at://${verifier1}/app.bsky.graph.verification/`,
|
|
242
233
|
],
|
|
243
234
|
},
|
|
235
|
+
{
|
|
236
|
+
description:
|
|
237
|
+
'returns undefined for user with invalid handle even if they have verifications',
|
|
238
|
+
getDid: () => handleinvalid,
|
|
239
|
+
getExpected: () => undefined,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
description:
|
|
243
|
+
'returns undefined for user with empty handle even if they have verifications',
|
|
244
|
+
getDid: () => handleempty,
|
|
245
|
+
getExpected: () => undefined,
|
|
246
|
+
},
|
|
244
247
|
]
|
|
245
248
|
|
|
246
249
|
it.each(testCases)(
|
|
@@ -277,25 +280,4 @@ describe('verification views', () => {
|
|
|
277
280
|
)
|
|
278
281
|
return res.data
|
|
279
282
|
}
|
|
280
|
-
|
|
281
|
-
const createLabel = async (opts: {
|
|
282
|
-
src?: string
|
|
283
|
-
uri: string
|
|
284
|
-
cid: string
|
|
285
|
-
val: string
|
|
286
|
-
exp?: string
|
|
287
|
-
}) => {
|
|
288
|
-
await network.bsky.db.db
|
|
289
|
-
.insertInto('label')
|
|
290
|
-
.values({
|
|
291
|
-
uri: opts.uri,
|
|
292
|
-
cid: opts.cid,
|
|
293
|
-
val: opts.val,
|
|
294
|
-
cts: new Date().toISOString(),
|
|
295
|
-
exp: opts.exp ?? null,
|
|
296
|
-
neg: false,
|
|
297
|
-
src: opts.src ?? labelerDid,
|
|
298
|
-
})
|
|
299
|
-
.execute()
|
|
300
|
-
}
|
|
301
283
|
})
|