From 5de4bc7b128a41eb9755f738da94ab8b0c2d5b47 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 20 May 2026 13:14:09 -0700 Subject: [PATCH 1/3] fix(notifications): personalize embedded users in related response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On initial app load the current user's follow relationship to other accounts (e.g. @audius) renders as not-following in lists hydrated from notification responses. Visiting that profile directly fixes the state. The bug: the /notifications/{user_id} endpoint embeds users in the related field, and the backend personalizes them via getMyId(c) which reads ?user_id= from the query string, not the URL path. The SDK was only sending the id in the path, so MyID resolved to 0 and the SQL short-circuited does_current_user_follow to false for every embedded user. The tan-query cache primed those entries on a cold notifications fetch and sticky-cached the wrong follow state until a personalized profile fetch overwrote them. The accompanying api PR adds an optional user_id query param to both /notifications/{user_id} and /notifications/{user_id}/playlist_updates. This change: - Regenerates @audius/sdk from the updated spec. The openapi generator renamed the new query param to `userId2` to avoid colliding with the path's `userId`. Wire format is correct (`queryParameters['user_id']`), the field name is just awkward — see PR description for follow-up options. - Threads the current user id through both call sites (useNotifications.ts, usePlaylistUpdates.ts) as `userId2` so the backend can compute personalization for related.users. Path and query carry the same value in the normal flow; they may diverge when a manager reads a managed user's notifications. The UsersApi.ts diff is incidental drift: getUserForYouFeed was removed upstream and our generated copy was stale. Co-Authored-By: Claude Opus 4.7 --- .../notifications/useNotifications.ts | 4 ++ .../playlist-updates/usePlaylistUpdates.ts | 6 +- .../default/apis/NotificationsApi.ts | 10 +++ .../api/generated/default/apis/UsersApi.ts | 64 ------------------- 4 files changed, 19 insertions(+), 65 deletions(-) diff --git a/packages/common/src/api/tan-query/notifications/useNotifications.ts b/packages/common/src/api/tan-query/notifications/useNotifications.ts index 0b24c22e041..97885d92fa5 100644 --- a/packages/common/src/api/tan-query/notifications/useNotifications.ts +++ b/packages/common/src/api/tan-query/notifications/useNotifications.ts @@ -77,6 +77,10 @@ export const useNotifications = (options?: QueryOptions) => { const sdk = await audiusSdk() const response = await sdk.notifications.getNotifications({ userId: Id.parse(currentUserId), + // Requester id sent as `?user_id=`. Needed so the backend personalizes + // embedded related.users (e.g. does_current_user_follow) — the path + // userId alone identifies the notifications owner, not the requester. + userId2: Id.parse(currentUserId), limit: DEFAULT_LIMIT, timestamp: pageParam?.timestamp, groupId: pageParam?.groupId diff --git a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts index 6c66ab09fdd..ab71c9960d0 100644 --- a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts +++ b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts @@ -38,14 +38,18 @@ export const usePlaylistUpdates = ( const sdk = await audiusSdk() // sdk.notifications.getPlaylistUpdates is not currently typed in the // public SDK surface; cast to the expected shape used in the legacy saga. + // userId2 carries the requester id as `?user_id=` so the backend can + // personalize related.users in the response. const response = (await ( sdk.notifications as { getPlaylistUpdates: (params: { userId: string + userId2?: string }) => Promise } ).getPlaylistUpdates({ - userId: Id.parse(currentUserId) + userId: Id.parse(currentUserId), + userId2: Id.parse(currentUserId) })) as PlaylistUpdatesResponse | undefined return transformAndCleanList( diff --git a/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts b/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts index 01246270381..eb8297dd889 100644 --- a/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts +++ b/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts @@ -27,6 +27,7 @@ import { export interface GetNotificationsRequest { userId: string; + userId2?: string; timestamp?: number; groupId?: string; limit?: number; @@ -35,6 +36,7 @@ export interface GetNotificationsRequest { export interface GetPlaylistUpdatesRequest { userId: string; + userId2?: string; } /** @@ -53,6 +55,10 @@ export class NotificationsApi extends runtime.BaseAPI { const queryParameters: any = {}; + if (params.userId2 !== undefined) { + queryParameters['user_id'] = params.userId2; + } + if (params.timestamp !== undefined) { queryParameters['timestamp'] = params.timestamp; } @@ -100,6 +106,10 @@ export class NotificationsApi extends runtime.BaseAPI { const queryParameters: any = {}; + if (params.userId2 !== undefined) { + queryParameters['user_id'] = params.userId2; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ diff --git a/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts b/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts index 3314b15d7e1..c2b8fb2e693 100644 --- a/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts +++ b/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts @@ -686,14 +686,6 @@ export interface GetUserFeedRequest { encodedDataSignature?: string; } -export interface GetUserForYouFeedRequest { - id: string; - limit?: number; - offset?: number; - maxPerArtist?: number; - userId?: string; -} - export interface GetUserIDsByAddressesRequest { address: Array; } @@ -4175,62 +4167,6 @@ export class UsersApi extends runtime.BaseAPI { return await response.value(); } - /** - * @hidden - * Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground, similar-artist) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead). - * Get For You feed for user - */ - async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (params.id === null || params.id === undefined) { - throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getUserForYouFeed.'); - } - - const queryParameters: any = {}; - - if (params.limit !== undefined) { - queryParameters['limit'] = params.limit; - } - - if (params.offset !== undefined) { - queryParameters['offset'] = params.offset; - } - - if (params.maxPerArtist !== undefined) { - queryParameters['max_per_artist'] = params.maxPerArtist; - } - - if (params.userId !== undefined) { - queryParameters['user_id'] = params.userId; - } - - const headerParameters: runtime.HTTPHeaders = {}; - - if (!headerParameters["Authorization"] && this.configuration && this.configuration.accessToken) { - const token = await this.configuration.accessToken("OAuth2", ["read"]); - if (token) { - headerParameters["Authorization"] = token; - } - } - - const response = await this.request({ - path: `/users/{id}/feed/for-you`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TracksFromJSON(jsonValue)); - } - - /** - * Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground, similar-artist) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead). - * Get For You feed for user - */ - async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getUserForYouFeedRaw(params, initOverrides); - return await response.value(); - } - /** * @hidden * Gets User IDs from any Ethereum wallet address or Solana account address associated with their Audius account. From 3e7cf506d776e179e8139b27b4c72c950bf6df88 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 20 May 2026 14:07:09 -0700 Subject: [PATCH 2/3] chore(notifications): regen SDK against renamed path placeholder Picks up AudiusProject/api#838, which renamed the OpenAPI path placeholder on /notifications/{user_id} and /notifications/{user_id}/playlist_updates to /notifications/{id} and /notifications/{id}/playlist_updates. With the collision against the query param gone, the generator now emits clean shapes: GetNotificationsRequest { id: string; userId?: string; ... } GetPlaylistUpdatesRequest { id: string; userId?: string } Update the three call sites to use { id, userId } instead of the previous awkward { userId, userId2 }. Concrete URL is unchanged. Co-Authored-By: Claude Opus 4.7 --- .../useNotificationUnreadCount.ts | 4 +-- .../notifications/useNotifications.ts | 8 +++--- .../playlist-updates/usePlaylistUpdates.ts | 10 +++---- .../default/apis/NotificationsApi.ts | 28 +++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts b/packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts index 8dd6b3a2732..6a6905b0bce 100644 --- a/packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts +++ b/packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts @@ -35,14 +35,14 @@ export const useNotificationUnreadCount = () => { const response = await ( sdk.notifications as { getNotifications: (params: { - userId: string + id: string limit?: number }) => Promise<{ data?: { unreadCount?: number } }> } ).getNotifications({ - userId: Id.parse(currentUserId), + id: Id.parse(currentUserId), limit: 0 }) return response?.data?.unreadCount ?? 0 diff --git a/packages/common/src/api/tan-query/notifications/useNotifications.ts b/packages/common/src/api/tan-query/notifications/useNotifications.ts index 97885d92fa5..96782a04b85 100644 --- a/packages/common/src/api/tan-query/notifications/useNotifications.ts +++ b/packages/common/src/api/tan-query/notifications/useNotifications.ts @@ -76,11 +76,11 @@ export const useNotifications = (options?: QueryOptions) => { queryFn: async ({ pageParam = null }) => { const sdk = await audiusSdk() const response = await sdk.notifications.getNotifications({ + id: Id.parse(currentUserId), + // Requester id sent as `?user_id=` so the backend personalizes + // embedded related.users (e.g. does_current_user_follow). The path + // id alone identifies the notifications owner, not the requester. userId: Id.parse(currentUserId), - // Requester id sent as `?user_id=`. Needed so the backend personalizes - // embedded related.users (e.g. does_current_user_follow) — the path - // userId alone identifies the notifications owner, not the requester. - userId2: Id.parse(currentUserId), limit: DEFAULT_LIMIT, timestamp: pageParam?.timestamp, groupId: pageParam?.groupId diff --git a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts index ab71c9960d0..6a090008574 100644 --- a/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts +++ b/packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts @@ -38,18 +38,18 @@ export const usePlaylistUpdates = ( const sdk = await audiusSdk() // sdk.notifications.getPlaylistUpdates is not currently typed in the // public SDK surface; cast to the expected shape used in the legacy saga. - // userId2 carries the requester id as `?user_id=` so the backend can + // userId carries the requester id as `?user_id=` so the backend can // personalize related.users in the response. const response = (await ( sdk.notifications as { getPlaylistUpdates: (params: { - userId: string - userId2?: string + id: string + userId?: string }) => Promise } ).getPlaylistUpdates({ - userId: Id.parse(currentUserId), - userId2: Id.parse(currentUserId) + id: Id.parse(currentUserId), + userId: Id.parse(currentUserId) })) as PlaylistUpdatesResponse | undefined return transformAndCleanList( diff --git a/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts b/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts index eb8297dd889..64d05ae27ad 100644 --- a/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts +++ b/packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts @@ -26,8 +26,8 @@ import { } from '../models'; export interface GetNotificationsRequest { - userId: string; - userId2?: string; + id: string; + userId?: string; timestamp?: number; groupId?: string; limit?: number; @@ -35,8 +35,8 @@ export interface GetNotificationsRequest { } export interface GetPlaylistUpdatesRequest { - userId: string; - userId2?: string; + id: string; + userId?: string; } /** @@ -49,14 +49,14 @@ export class NotificationsApi extends runtime.BaseAPI { * Get notifications for user ID */ async getNotificationsRaw(params: GetNotificationsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (params.userId === null || params.userId === undefined) { - throw new runtime.RequiredError('userId','Required parameter params.userId was null or undefined when calling getNotifications.'); + if (params.id === null || params.id === undefined) { + throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getNotifications.'); } const queryParameters: any = {}; - if (params.userId2 !== undefined) { - queryParameters['user_id'] = params.userId2; + if (params.userId !== undefined) { + queryParameters['user_id'] = params.userId; } if (params.timestamp !== undefined) { @@ -78,7 +78,7 @@ export class NotificationsApi extends runtime.BaseAPI { const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ - path: `/notifications/{user_id}`.replace(`{${"user_id"}}`, encodeURIComponent(String(params.userId))), + path: `/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))), method: 'GET', headers: headerParameters, query: queryParameters, @@ -100,20 +100,20 @@ export class NotificationsApi extends runtime.BaseAPI { * Get playlists the user has saved that have been updated for user ID */ async getPlaylistUpdatesRaw(params: GetPlaylistUpdatesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (params.userId === null || params.userId === undefined) { - throw new runtime.RequiredError('userId','Required parameter params.userId was null or undefined when calling getPlaylistUpdates.'); + if (params.id === null || params.id === undefined) { + throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getPlaylistUpdates.'); } const queryParameters: any = {}; - if (params.userId2 !== undefined) { - queryParameters['user_id'] = params.userId2; + if (params.userId !== undefined) { + queryParameters['user_id'] = params.userId; } const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ - path: `/notifications/{user_id}/playlist_updates`.replace(`{${"user_id"}}`, encodeURIComponent(String(params.userId))), + path: `/notifications/{id}/playlist_updates`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))), method: 'GET', headers: headerParameters, query: queryParameters, From 77d303ff167b2cd3c4d49d8c2c88c57ff75b4a93 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 20 May 2026 14:57:33 -0700 Subject: [PATCH 3/3] chore(changeset): @audius/sdk major for notifications path field rename Co-Authored-By: Claude Opus 4.7 --- .changeset/notifications-path-id-rename.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .changeset/notifications-path-id-rename.md diff --git a/.changeset/notifications-path-id-rename.md b/.changeset/notifications-path-id-rename.md new file mode 100644 index 00000000000..877f8e70010 --- /dev/null +++ b/.changeset/notifications-path-id-rename.md @@ -0,0 +1,22 @@ +--- +'@audius/sdk': major +--- + +Rename the path field on `getNotifications` and `getPlaylistUpdates` from `userId` to `id`, matching the convention used by every `/users/{id}/…` method. Add an optional `userId` query field that carries the requester id for personalization of embedded `related.users` (e.g. `does_current_user_follow`). + +**Migration:** + +```ts +// Before +sdk.notifications.getNotifications({ userId: 'aE9MA' }) +sdk.notifications.getPlaylistUpdates({ userId: 'aE9MA' }) + +// After +sdk.notifications.getNotifications({ + id: 'aE9MA', // notifications owner (was `userId`) + userId: 'aE9MA' // requester id, for personalization of related.users +}) +sdk.notifications.getPlaylistUpdates({ id: 'aE9MA', userId: 'aE9MA' }) +``` + +The two ids differ only when a manager reads a managed user's notifications; in the normal flow they're the same value. The wire format and server URL are unchanged — only the request type shape was renamed to remove a collision between the path and the new query parameter.