Compare commits

...

4 Commits

Author SHA1 Message Date
Devin Ivy 0a68b716c3 bskyweb: do not serve cache headers on non-2XX for static assets 2025-01-16 17:49:47 -08:00
Hailey d82cd9bdb3 add new events 2025-01-16 15:44:02 -08:00
Eric Bailey abbd7461a1 Only apply CDN fix to webpack in prod 2025-01-15 13:46:13 -08:00
Hailey 004522466e add back statsig gate testing (#7404) 2025-01-10 09:51:33 -08:00
15 changed files with 217 additions and 23 deletions
+13 -7
View File
@@ -214,15 +214,21 @@ func serve(cctx *cli.Context) error {
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
c.Response().Before(func() {
if c.Response().Status >= 300 {
return
}
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
})
return next(c)
}
})
+1 -1
View File
@@ -54,7 +54,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.13.21",
"@atproto/api": "^0.13.27",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
+23 -1
View File
@@ -196,6 +196,7 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
<ProfileGrid
isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []}
recId={data?.recId}
error={error}
viewContext="profile"
/>
@@ -222,10 +223,12 @@ export function ProfileGrid({
isSuggestionsLoading,
error,
profiles,
recId,
viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: AppBskyActorDefs.ProfileViewDetailed[]
recId?: number
error: Error | null
viewContext: 'profile' | 'feed'
}) {
@@ -249,12 +252,20 @@ export function ProfileGrid({
))
) : error || !profiles.length ? null : (
<>
{profiles.slice(0, maxLength).map(profile => (
{profiles.slice(0, maxLength).map((profile, index) => (
<ProfileCard.Link
key={profile.did}
profile={profile}
onPress={() => {
logEvent('feed:interstitial:profileCard:press', {})
logEvent('suggestedUser:press', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
recId,
position: index,
})
}}
style={[
a.flex_1,
@@ -282,6 +293,17 @@ export function ProfileGrid({
logContext="FeedInterstitial"
shape="round"
colorInverted
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
+3
View File
@@ -286,6 +286,7 @@ export type FollowButtonProps = {
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
colorInverted?: boolean
onFollow?: () => void
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
@@ -299,6 +300,7 @@ export function FollowButtonInner({
moderationOpts,
logContext,
onPress: onPressProp,
onFollow,
colorInverted,
...rest
}: FollowButtonProps) {
@@ -325,6 +327,7 @@ export function FollowButtonInner({
),
)
onPressProp?.(e)
onFollow?.()
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
+3
View File
@@ -63,6 +63,9 @@ export function IS_PROD_SERVICE(url?: string) {
export const PROD_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
export const STAGING_DEFAULT_FEED = (rkey: string) =>
`at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/${rkey}`
export const POST_IMG_MAX = {
width: 2000,
height: 2000,
+20
View File
@@ -164,6 +164,26 @@ export type LogEvents = {
| 'ProfileHeaderSuggestedFollows'
| 'PostOnboardingFindFollows'
}
'suggestedUser:follow': {
logContext:
| 'Explore'
| 'InterstitialDiscover'
| 'InterstitialProfile'
| 'Profile'
location: 'Card' | 'Profile'
recId?: number
position: number
}
'suggestedUser:press': {
logContext: 'Explore' | 'InterstitialDiscover' | 'InterstitialProfile'
recId?: number
position: number
}
'suggestedUser:seen': {
logContext: 'Explore' | 'InterstitialDiscover' | 'InterstitialProfile'
recId?: number
position: number
}
'profile:unfollow': {
logContext:
| 'RecommendedFollowsItem'
+2
View File
@@ -4,4 +4,6 @@ export type Gate =
| 'debug_subscriptions'
| 'new_postonboarding'
| 'remove_show_latest_button'
| 'test_gate_1'
| 'test_gate_2'
| 'trending_topics_beta'
+6 -3
View File
@@ -3,7 +3,7 @@ import {AppState, AppStateStatus} from 'react-native'
import {AppBskyFeedDefs} from '@atproto/api'
import throttle from 'lodash.throttle'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {PROD_DEFAULT_FEED, STAGING_DEFAULT_FEED} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
@@ -54,7 +54,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
encoding: 'application/json',
headers: {
// TODO when we start sending to other feeds, we need to grab their DID -prf
'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg',
'atproto-proxy': 'did:web:algo.pop2.bsky.app#bsky_fg',
},
},
)
@@ -155,7 +155,10 @@ export function useFeedFeedbackContext() {
// place, we're hardcoding it to the discover feed.
// -prf
function isDiscoverFeed(feed: FeedDescriptor) {
return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}`
return (
feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}` ||
feed === `feedgen|${STAGING_DEFAULT_FEED('thevids')}`
)
}
function toString(interaction: AppBskyFeedDefs.Interaction): string {
+1 -1
View File
@@ -120,7 +120,7 @@ export function useSuggestedFollowsByActorQuery({
const suggestions = res.data.isFallback
? []
: res.data.suggestions.filter(profile => !profile.viewer?.following)
return {suggestions}
return {suggestions, recId: res.data.recId}
},
enabled,
})
+3
View File
@@ -14,12 +14,14 @@ export function FollowButton({
profile,
labelStyle,
logContext,
onFollow,
}: {
unfollowedType?: ButtonType
followedType?: ButtonType
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
labelStyle?: StyleProp<TextStyle>
logContext: 'ProfileCard' | 'StarterPackProfilesList'
onFollow?: () => void
}) {
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
@@ -30,6 +32,7 @@ export function FollowButton({
const onPressFollow = async () => {
try {
await queueFollow()
onFollow?.()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
+7 -1
View File
@@ -184,6 +184,7 @@ export function ProfileCardWithFollowBtn({
noBg,
noBorder,
onPress,
onFollow,
logContext = 'ProfileCard',
showKnownFollowers,
}: {
@@ -191,6 +192,7 @@ export function ProfileCardWithFollowBtn({
noBg?: boolean
noBorder?: boolean
onPress?: () => void
onFollow?: () => void
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
showKnownFollowers?: boolean
}) {
@@ -206,7 +208,11 @@ export function ProfileCardWithFollowBtn({
isMe
? undefined
: profileShadow => (
<FollowButton profile={profileShadow} logContext={logContext} />
<FollowButton
profile={profileShadow}
logContext={logContext}
onFollow={onFollow}
/>
)
}
onPress={onPress}
+77 -1
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useMemo} from 'react'
import React, {useCallback, useEffect, useMemo} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {
@@ -20,6 +20,7 @@ import {
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {combinedDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
import {isInvalidHandle} from '#/lib/strings/handles'
@@ -496,6 +497,7 @@ function ProfileScreenLoaded({
accessibilityHint=""
/>
)}
<TestGates />
</ScreenHider>
)
}
@@ -554,3 +556,77 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
})
const shouldExposeToGate2 = Math.random() < 0.2
// --- Temporary: we're testing our Statsig setup ---
let TestGates = React.memo(function TestGates() {
const gate = useGate()
useEffect(() => {
logEvent('test:all:always', {})
if (Math.random() < 0.2) {
logEvent('test:all:sometimes', {})
}
if (Math.random() < 0.1) {
logEvent('test:all:boosted_by_gate1', {
reason: 'base',
})
}
if (Math.random() < 0.1) {
logEvent('test:all:boosted_by_gate2', {
reason: 'base',
})
}
if (Math.random() < 0.1) {
logEvent('test:all:boosted_by_both', {
reason: 'base',
})
}
}, [])
return [
gate('test_gate_1') ? <TestGate1 /> : null,
shouldExposeToGate2 && gate('test_gate_2') ? <TestGate2 /> : null,
]
})
function TestGate1() {
useEffect(() => {
logEvent('test:gate1:always', {})
if (Math.random() < 0.2) {
logEvent('test:gate1:sometimes', {})
}
if (Math.random() < 0.5) {
logEvent('test:all:boosted_by_gate1', {
reason: 'gate1',
})
}
if (Math.random() < 0.5) {
logEvent('test:all:boosted_by_both', {
reason: 'gate1',
})
}
}, [])
return null
}
function TestGate2() {
useEffect(() => {
logEvent('test:gate2:always', {})
if (Math.random() < 0.2) {
logEvent('test:gate2:sometimes', {})
}
if (Math.random() < 0.5) {
logEvent('test:all:boosted_by_gate2', {
reason: 'gate2',
})
}
if (Math.random() < 0.5) {
logEvent('test:all:boosted_by_both', {
reason: 'gate2',
})
}
}, [])
return null
}
+19 -1
View File
@@ -10,6 +10,7 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
@@ -253,6 +254,7 @@ type ExploreScreenItems =
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileView
recId?: number
}
| {
type: 'feed'
@@ -370,6 +372,7 @@ export function Explore() {
type: 'profile',
key: actor.did,
profile: actor,
recId: page.recId,
})
}
}
@@ -498,7 +501,7 @@ export function Explore() {
])
const renderItem = React.useCallback(
({item}: {item: ExploreScreenItems}) => {
({item, index}: {item: ExploreScreenItems; index: number}) => {
switch (item.type) {
case 'header': {
return (
@@ -524,6 +527,21 @@ export function Explore() {
noBg
noBorder
showKnownFollowers
onPress={() => {
logEvent('suggestedUser:press', {
logContext: 'Explore',
recId: item.recId,
position: index,
})
}}
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext: 'Explore',
location: 'Card',
recId: item.recId,
position: index,
})
}}
/>
</View>
)
+3
View File
@@ -28,6 +28,9 @@ module.exports = async function (env, argv) {
]
if (env.mode === 'development') {
config.plugins.push(new ReactRefreshWebpackPlugin())
} else {
// Support static CDN for chunks
config.output.publicPath = 'auto'
}
if (GENERATE_STATS || OPEN_ANALYZER) {
+36 -7
View File
@@ -72,15 +72,15 @@
tlds "^1.234.0"
zod "^3.23.8"
"@atproto/api@^0.13.21":
version "0.13.21"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.21.tgz#8ee27a07e5a024b5bf32408d9bd623dd598ad1cc"
integrity sha512-iOxSj2YS3Fx9IPz1NivKrSsdYPNbBgpnUH7+WhKYAMvDFDUe2PZe7taau8wsUjJAu/H3S0Mk2TDh5e/7tCRwHA==
"@atproto/api@^0.13.27":
version "0.13.27"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.27.tgz#54d1172fbc4415d41089d2715a657052b26b60bc"
integrity sha512-IaZezARJSoFBxFvutGa+2hT0NiA+db6NpKkla2bRyv/SPinYViuqD5kaaPzc732yeE1uwUKQc4wwwlctYamFBQ==
dependencies:
"@atproto/common-web" "^0.3.1"
"@atproto/lexicon" "^0.4.4"
"@atproto/common-web" "^0.3.2"
"@atproto/lexicon" "^0.4.5"
"@atproto/syntax" "^0.3.1"
"@atproto/xrpc" "^0.6.5"
"@atproto/xrpc" "^0.6.6"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
@@ -169,6 +169,16 @@
uint8arrays "3.0.0"
zod "^3.23.8"
"@atproto/common-web@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.2.tgz#4cf78ad4d24fed801882f3d35afc39bceccdff51"
integrity sha512-Vx0JtL1/CssJbFAb0UOdvTrkbUautsDfHNOXNTcX2vyPIxH9xOameSqLLunM1hZnOQbJwyjmQCt6TV+bhnanDg==
dependencies:
graphemer "^1.4.0"
multiformats "^9.9.0"
uint8arrays "3.0.0"
zod "^3.23.8"
"@atproto/common@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
@@ -283,6 +293,17 @@
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/lexicon@^0.4.5":
version "0.4.5"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.5.tgz#4fcf3731193c674286e9e8d677bbab5dd530b817"
integrity sha512-fljWqMGKn+XWtTprBcS3F1hGBREnQYh6qYHv2sjENucc7REms1gtmZXSerB9N6pVeHVNOnXiILdukeAcic5OEw==
dependencies:
"@atproto/common-web" "^0.3.2"
"@atproto/syntax" "^0.3.1"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/oauth-provider@^0.2.10":
version "0.2.10"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901"
@@ -452,6 +473,14 @@
"@atproto/lexicon" "^0.4.4"
zod "^3.23.8"
"@atproto/xrpc@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.6.tgz#28f58270ef4a8056f7f718bd52512e74bcd3702f"
integrity sha512-umXEYVMo9/pyIBoKmIAIi64RXDW9tSXY+wqztlQ6I2GZtjLfNZqmAWU+wADk3SxUe54mvjxxGyA4TtyGtDMfhA==
dependencies:
"@atproto/lexicon" "^0.4.5"
zod "^3.23.8"
"@aws-crypto/crc32@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"