Skip to content

Commit 33c7e8a

Browse files
authored
feat(timeToRead): [DIS-852] Add time_to_read / timeToRead (#56)
* feat(timeToRead): [DIS-852] Add time_to_read / timeToRead * fix(timeToRead): Lint error * chore(timeToRead): Convert mock to early exit
1 parent 7065fbb commit 33c7e8a

File tree

11 files changed

+109
-32
lines changed

11 files changed

+109
-32
lines changed

openapi.yml

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ components:
152152
imageUrl:
153153
type: string
154154
description: The primary image for a Recommendation.
155+
timeToRead:
156+
type: integer
157+
description: Article read time in minutes
155158

156159
LegacyFeedItem:
157160
type: object
@@ -178,6 +181,8 @@ components:
178181
type: string
179182
raw_image_src:
180183
type: string
184+
time_to_read:
185+
type: integer
181186

182187
LegacySettings:
183188
type: object
@@ -300,21 +305,6 @@ paths:
300305
description: This region string is Fx domain language, and built from Fx expectations. Parameter values are not case sensitive. See [Firefox Home & New Tab Regional Differences](https://mozilla-hub.atlassian.net/wiki/spaces/FPS/pages/80448805/Regional+Differences).
301306
schema:
302307
type: string
303-
enum: [
304-
# relevant docs: https://docs.google.com/document/d/1omclr-eETJ7zAWTMI7mvvsc3_-ns2Iiho4jPEfrmZfo
305-
US,
306-
CA,
307-
DE,
308-
GB,
309-
IE,
310-
FR,
311-
ES,
312-
IT,
313-
IN,
314-
CH,
315-
AT,
316-
BE,
317-
]
318308
responses:
319309
'200':
320310
description: OK

src/api/desktop/recommendations/recommendations.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,8 @@ describe('recommendations API server', () => {
8989
expect(recommendation.tileId).toEqual(
9090
mockResponse.newTabSlate.recommendations[0].tileId
9191
);
92+
if (recommendation.timeToRead !== undefined) {
93+
expect(recommendation.timeToRead).toBeGreaterThanOrEqual(1);
94+
}
9295
});
9396
});

src/api/desktop/recommendations/response.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ describe('response', () => {
5252
`utm_source=${graphResponse.newTabSlate.utmSource}`
5353
)
5454
).toBeTruthy();
55+
// Even recommendations have timeToRead mocked to [1, 9].
56+
expect(res.data[0].timeToRead).toBeGreaterThanOrEqual(1);
57+
expect(res.data[0].timeToRead).toBeLessThanOrEqual(9);
58+
// Odd recommendations have timeToRead mocked to undefined.
59+
expect(res.data[1].timeToRead).toBeUndefined();
5560
} else {
5661
throw validate.errors;
5762
}

src/api/desktop/recommendations/response.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Logger } from '../../../logger';
44
import { Unpack } from '../../../types';
55

66
// unpack GraphQL generated type for 'recommendations' from NewTabRecommendationsQuery
7-
type GraphRecommendation = Unpack<
7+
export type GraphRecommendation = Unpack<
88
NewTabRecommendationsQuery['newTabSlate']['recommendations']
99
>;
1010

@@ -44,7 +44,7 @@ export const mapRecommendation = (
4444
recommendation: GraphRecommendation,
4545
utmSource: string
4646
): Recommendation => {
47-
return {
47+
const recommendationToReturn: Recommendation = {
4848
__typename: 'Recommendation',
4949
tileId: recommendation.tileId,
5050
url: appendUtmSource(
@@ -56,6 +56,15 @@ export const mapRecommendation = (
5656
publisher: recommendation.corpusItem.publisher,
5757
imageUrl: recommendation.corpusItem.imageUrl,
5858
};
59+
60+
if (recommendation.corpusItem.timeToRead) {
61+
return {
62+
...recommendationToReturn,
63+
timeToRead: recommendation.corpusItem.timeToRead,
64+
};
65+
}
66+
67+
return recommendationToReturn;
5968
};
6069

6170
export const responseTransformer = (

src/api/v3/recommendations.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('v3 legacy recommendations API server', () => {
6464

6565
expect(res.status).toEqual(200);
6666

67-
// response ins json
67+
// response is json
6868
const parsedRes = JSON.parse(res.text);
6969
expect(parsedRes.recommendations?.length).toEqual(1);
7070
const recommendation: LegacyFeedItem = parsedRes.recommendations[0];
@@ -74,5 +74,8 @@ describe('v3 legacy recommendations API server', () => {
7474
expect(recommendation.id).toEqual(
7575
mockResponse.newTabSlate.recommendations[0].tileId
7676
);
77+
if (recommendation.time_to_read !== undefined) {
78+
expect(recommendation.time_to_read).toBeGreaterThanOrEqual(1);
79+
}
7780
});
7881
});

src/api/v3/response.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ describe('response', () => {
5252
`utm_source=${graphResponse.newTabSlate.utmSource}`
5353
)
5454
).toBeTruthy();
55+
// Even recommendations have timeToRead mocked to [1, 9].
56+
expect(res.recommendations[0].time_to_read).toBeGreaterThanOrEqual(1);
57+
expect(res.recommendations[0].time_to_read).toBeLessThanOrEqual(9);
58+
// Odd recommendations have timeToRead mocked to undefined.
59+
expect(res.recommendations[1].time_to_read).toBeUndefined();
5560
} else {
5661
throw validate.errors;
5762
}

src/api/v3/response.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const mapRecommendation = (
2424
recommendation.corpusItem.imageUrl
2525
);
2626

27-
return {
27+
const feedItemToReturn: LegacyFeedItem = {
2828
id: recommendation.tileId,
2929
url: appendUtmSource(
3030
recommendation.corpusItem.url,
@@ -36,6 +36,15 @@ export const mapRecommendation = (
3636
raw_image_src: recommendation.corpusItem.imageUrl,
3737
image_src: `https://img-getpocket.cdn.mozilla.net/direct?url=${encodedImageUrl}&resize=w450`,
3838
};
39+
40+
if (recommendation.corpusItem.timeToRead) {
41+
return {
42+
...feedItemToReturn,
43+
time_to_read: recommendation.corpusItem.timeToRead,
44+
};
45+
}
46+
47+
return feedItemToReturn;
3948
};
4049

4150
export const responseTransformer = (

src/generated/graphql/types.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ export type CorpusItem = {
245245
shortUrl?: Maybe<Scalars['Url']>;
246246
/** If the Corpus Item is pocket owned with a specific type, this is the associated object (Collection or SyndicatedArticle). */
247247
target?: Maybe<CorpusTarget>;
248+
/** Time to read in minutes. Is nullable. */
249+
timeToRead?: Maybe<Scalars['Int']>;
248250
/** The title of the Approved Item. */
249251
title: Scalars['String'];
250252
/** The topic associated with the Approved Item. */
@@ -429,6 +431,14 @@ export type DomainMetadata = {
429431
name?: Maybe<Scalars['String']>;
430432
};
431433

434+
/** The reason a user web session is being expired. */
435+
export enum ExpireUserWebSessionReason {
436+
/** Expire web session upon logging out. */
437+
Logout = 'LOGOUT',
438+
/** Expire web session on account password change. */
439+
PasswordChanged = 'PASSWORD_CHANGED'
440+
}
441+
432442
/** Input field to boost the score of an elasticsearch document based on a specific field and value */
433443
export type FunctionalBoostField = {
434444
/** A float number to boost the score by */
@@ -872,6 +882,12 @@ export type Mutation = {
872882
* Returns firefox account ID sent as the query parameter with the request.
873883
*/
874884
deleteUserByFxaId: Scalars['ID'];
885+
/**
886+
* Expires a user's web session tokens by firefox account ID.
887+
* Called by fxa-webhook proxy. Need to supply a reason why to expire user web session.
888+
* Returns the user ID.
889+
*/
890+
expireUserWebSessionByFxaId: Scalars['ID'];
875891
/**
876892
* temporary mutation for apple user migration.
877893
* called by fxa-webhook proxy to update the fxaId and email of the user.
@@ -1086,6 +1102,13 @@ export type MutationDeleteUserByFxaIdArgs = {
10861102
};
10871103

10881104

1105+
/** Default Mutation Type */
1106+
export type MutationExpireUserWebSessionByFxaIdArgs = {
1107+
id: Scalars['ID'];
1108+
reason: ExpireUserWebSessionReason;
1109+
};
1110+
1111+
10891112
/** Default Mutation Type */
10901113
export type MutationMigrateAppleUserArgs = {
10911114
email: Scalars['String'];
@@ -2800,6 +2823,8 @@ export type User = {
28002823
email?: Maybe<Scalars['String']>;
28012824
/** The users first name */
28022825
firstName?: Maybe<Scalars['String']>;
2826+
/** Indicates if a user is FxA or not */
2827+
isFxa?: Maybe<Scalars['Boolean']>;
28032828
/** The user's premium status */
28042829
isPremium?: Maybe<Scalars['Boolean']>;
28052830
/** The users last name */
@@ -2938,8 +2963,8 @@ export type NewTabRecommendationsQueryVariables = Exact<{
29382963
}>;
29392964

29402965

2941-
export type NewTabRecommendationsQuery = { __typename?: 'Query', newTabSlate: { __typename?: 'CorpusSlate', utmSource?: string | null, recommendations: Array<{ __typename?: 'CorpusRecommendation', tileId: number, corpusItem: { __typename?: 'CorpusItem', excerpt: string, imageUrl: any, publisher: string, title: string, url: any } }> } };
2966+
export type NewTabRecommendationsQuery = { __typename?: 'Query', newTabSlate: { __typename?: 'CorpusSlate', utmSource?: string | null, recommendations: Array<{ __typename?: 'CorpusRecommendation', tileId: number, corpusItem: { __typename?: 'CorpusItem', excerpt: string, imageUrl: any, publisher: string, title: string, url: any, timeToRead?: number | null } }> } };
29422967

29432968

29442969
export const RecentSavesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RecentSaves"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"statuses"},"value":{"kind":"ListValue","values":[{"kind":"EnumValue","value":"UNREAD"}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"sort"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"sortBy"},"value":{"kind":"EnumValue","value":"CREATED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"sortOrder"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"item"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Item"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"wordCount"}},{"kind":"Field","name":{"kind":"Name","value":"topImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"timeToRead"}},{"kind":"Field","name":{"kind":"Name","value":"resolvedUrl"}},{"kind":"Field","name":{"kind":"Name","value":"givenUrl"}},{"kind":"Field","name":{"kind":"Name","value":"excerpt"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}}]}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<RecentSavesQuery, RecentSavesQueryVariables>;
2945-
export const NewTabRecommendationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NewTabRecommendations"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"region"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"count"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newTabSlate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locale"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}},{"kind":"Argument","name":{"kind":"Name","value":"region"},"value":{"kind":"Variable","name":{"kind":"Name","value":"region"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"utmSource"}},{"kind":"Field","name":{"kind":"Name","value":"recommendations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"count"},"value":{"kind":"Variable","name":{"kind":"Name","value":"count"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tileId"}},{"kind":"Field","name":{"kind":"Name","value":"corpusItem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"excerpt"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"publisher"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]}}]}}]} as unknown as DocumentNode<NewTabRecommendationsQuery, NewTabRecommendationsQueryVariables>;
2970+
export const NewTabRecommendationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NewTabRecommendations"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"region"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"count"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newTabSlate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locale"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}},{"kind":"Argument","name":{"kind":"Name","value":"region"},"value":{"kind":"Variable","name":{"kind":"Name","value":"region"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"utmSource"}},{"kind":"Field","name":{"kind":"Name","value":"recommendations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"count"},"value":{"kind":"Variable","name":{"kind":"Name","value":"count"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tileId"}},{"kind":"Field","name":{"kind":"Name","value":"corpusItem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"excerpt"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"publisher"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"timeToRead"}}]}}]}}]}}]}}]} as unknown as DocumentNode<NewTabRecommendationsQuery, NewTabRecommendationsQueryVariables>;

src/generated/openapi/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export interface components {
113113
publisher: string;
114114
/** @description The primary image for a Recommendation. */
115115
imageUrl: string;
116+
/** @description Article read time in minutes */
117+
timeToRead?: number;
116118
};
117119
LegacyFeedItem: {
118120
id: number;
@@ -122,6 +124,7 @@ export interface components {
122124
domain: string;
123125
image_src: string;
124126
raw_image_src: string;
127+
time_to_read?: number;
125128
};
126129
LegacySettings: {
127130
spocsPerNewTabs?: number;

src/graphql-proxy/recommendations/Recommendations.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ query NewTabRecommendations($locale: String!, $region: String, $count: Int) {
99
publisher
1010
title
1111
url
12+
timeToRead
1213
}
1314
}
1415
}

0 commit comments

Comments
 (0)