Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ components:
imageUrl:
type: string
description: The primary image for a Recommendation.
timeToRead:
type: integer
description: Article read time in minutes
Comment on lines +155 to +157
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ScottDowne Just to confirm, timeToRead (camel-case) is supported by Firefox when hitting the new api, right?

Seems like that's the case looking at the code: https://searchfox.org/mozilla-central/source/browser/components/pocket/content/pktApi.sys.mjs#868

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct.


LegacyFeedItem:
type: object
Expand All @@ -178,6 +181,8 @@ components:
type: string
raw_image_src:
type: string
time_to_read:
type: integer

LegacySettings:
type: object
Expand Down Expand Up @@ -300,21 +305,6 @@ paths:
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).
schema:
type: string
enum: [
# relevant docs: https://docs.google.com/document/d/1omclr-eETJ7zAWTMI7mvvsc3_-ns2Iiho4jPEfrmZfo
US,
CA,
DE,
GB,
IE,
FR,
ES,
IT,
IN,
CH,
AT,
BE,
]
Copy link
Contributor Author

@mmiermans mmiermans Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated to time-to-read, but we just happened to notice that #55 accidentally re-introduced this enum. The generated code was not impacted by this, so requests from unsupported regions such as NL continue to work.

responses:
'200':
description: OK
Expand Down
3 changes: 3 additions & 0 deletions src/api/desktop/recommendations/recommendations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,8 @@ describe('recommendations API server', () => {
expect(recommendation.tileId).toEqual(
mockResponse.newTabSlate.recommendations[0].tileId
);
if (recommendation.timeToRead !== undefined) {
expect(recommendation.timeToRead).toBeGreaterThanOrEqual(1);
}
});
});
5 changes: 5 additions & 0 deletions src/api/desktop/recommendations/response.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ describe('response', () => {
`utm_source=${graphResponse.newTabSlate.utmSource}`
)
).toBeTruthy();
// Even recommendations have timeToRead mocked to [1, 9].
expect(res.data[0].timeToRead).toBeGreaterThanOrEqual(1);
expect(res.data[0].timeToRead).toBeLessThanOrEqual(9);
// Odd recommendations have timeToRead mocked to undefined.
expect(res.data[1].timeToRead).toBeUndefined();
} else {
throw validate.errors;
}
Expand Down
13 changes: 11 additions & 2 deletions src/api/desktop/recommendations/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Logger } from '../../../logger';
import { Unpack } from '../../../types';

// unpack GraphQL generated type for 'recommendations' from NewTabRecommendationsQuery
type GraphRecommendation = Unpack<
export type GraphRecommendation = Unpack<
NewTabRecommendationsQuery['newTabSlate']['recommendations']
>;

Expand Down Expand Up @@ -44,7 +44,7 @@ export const mapRecommendation = (
recommendation: GraphRecommendation,
utmSource: string
): Recommendation => {
return {
const recommendationToReturn: Recommendation = {
__typename: 'Recommendation',
tileId: recommendation.tileId,
url: appendUtmSource(
Expand All @@ -56,6 +56,15 @@ export const mapRecommendation = (
publisher: recommendation.corpusItem.publisher,
imageUrl: recommendation.corpusItem.imageUrl,
};

if (recommendation.corpusItem.timeToRead) {
return {
...recommendationToReturn,
timeToRead: recommendation.corpusItem.timeToRead,
};
}

return recommendationToReturn;
};

export const responseTransformer = (
Expand Down
5 changes: 4 additions & 1 deletion src/api/v3/recommendations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('v3 legacy recommendations API server', () => {

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

// response ins json
// response is json
const parsedRes = JSON.parse(res.text);
expect(parsedRes.recommendations?.length).toEqual(1);
const recommendation: LegacyFeedItem = parsedRes.recommendations[0];
Expand All @@ -74,5 +74,8 @@ describe('v3 legacy recommendations API server', () => {
expect(recommendation.id).toEqual(
mockResponse.newTabSlate.recommendations[0].tileId
);
if (recommendation.time_to_read !== undefined) {
expect(recommendation.time_to_read).toBeGreaterThanOrEqual(1);
}
});
});
5 changes: 5 additions & 0 deletions src/api/v3/response.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ describe('response', () => {
`utm_source=${graphResponse.newTabSlate.utmSource}`
)
).toBeTruthy();
// Even recommendations have timeToRead mocked to [1, 9].
expect(res.recommendations[0].time_to_read).toBeGreaterThanOrEqual(1);
expect(res.recommendations[0].time_to_read).toBeLessThanOrEqual(9);
// Odd recommendations have timeToRead mocked to undefined.
expect(res.recommendations[1].time_to_read).toBeUndefined();
} else {
throw validate.errors;
}
Expand Down
11 changes: 10 additions & 1 deletion src/api/v3/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const mapRecommendation = (
recommendation.corpusItem.imageUrl
);

return {
const feedItemToReturn: LegacyFeedItem = {
id: recommendation.tileId,
url: appendUtmSource(
recommendation.corpusItem.url,
Expand All @@ -36,6 +36,15 @@ export const mapRecommendation = (
raw_image_src: recommendation.corpusItem.imageUrl,
image_src: `https://img-getpocket.cdn.mozilla.net/direct?url=${encodedImageUrl}&resize=w450`,
};

if (recommendation.corpusItem.timeToRead) {
return {
...feedItemToReturn,
time_to_read: recommendation.corpusItem.timeToRead,
};
}

return feedItemToReturn;
};

export const responseTransformer = (
Expand Down
29 changes: 27 additions & 2 deletions src/generated/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export type CorpusItem = {
shortUrl?: Maybe<Scalars['Url']>;
/** If the Corpus Item is pocket owned with a specific type, this is the associated object (Collection or SyndicatedArticle). */
target?: Maybe<CorpusTarget>;
/** Time to read in minutes. Is nullable. */
timeToRead?: Maybe<Scalars['Int']>;
/** The title of the Approved Item. */
title: Scalars['String'];
/** The topic associated with the Approved Item. */
Expand Down Expand Up @@ -429,6 +431,14 @@ export type DomainMetadata = {
name?: Maybe<Scalars['String']>;
};

/** The reason a user web session is being expired. */
export enum ExpireUserWebSessionReason {
/** Expire web session upon logging out. */
Logout = 'LOGOUT',
/** Expire web session on account password change. */
PasswordChanged = 'PASSWORD_CHANGED'
}

/** Input field to boost the score of an elasticsearch document based on a specific field and value */
export type FunctionalBoostField = {
/** A float number to boost the score by */
Expand Down Expand Up @@ -872,6 +882,12 @@ export type Mutation = {
* Returns firefox account ID sent as the query parameter with the request.
*/
deleteUserByFxaId: Scalars['ID'];
/**
* Expires a user's web session tokens by firefox account ID.
* Called by fxa-webhook proxy. Need to supply a reason why to expire user web session.
* Returns the user ID.
*/
expireUserWebSessionByFxaId: Scalars['ID'];
/**
* temporary mutation for apple user migration.
* called by fxa-webhook proxy to update the fxaId and email of the user.
Expand Down Expand Up @@ -1086,6 +1102,13 @@ export type MutationDeleteUserByFxaIdArgs = {
};


/** Default Mutation Type */
export type MutationExpireUserWebSessionByFxaIdArgs = {
id: Scalars['ID'];
reason: ExpireUserWebSessionReason;
};


/** Default Mutation Type */
export type MutationMigrateAppleUserArgs = {
email: Scalars['String'];
Expand Down Expand Up @@ -2800,6 +2823,8 @@ export type User = {
email?: Maybe<Scalars['String']>;
/** The users first name */
firstName?: Maybe<Scalars['String']>;
/** Indicates if a user is FxA or not */
isFxa?: Maybe<Scalars['Boolean']>;
/** The user's premium status */
isPremium?: Maybe<Scalars['Boolean']>;
/** The users last name */
Expand Down Expand Up @@ -2938,8 +2963,8 @@ export type NewTabRecommendationsQueryVariables = Exact<{
}>;


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 } }> } };
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 } }> } };


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>;
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>;
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>;
3 changes: 3 additions & 0 deletions src/generated/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface components {
publisher: string;
/** @description The primary image for a Recommendation. */
imageUrl: string;
/** @description Article read time in minutes */
timeToRead?: number;
};
LegacyFeedItem: {
id: number;
Expand All @@ -122,6 +124,7 @@ export interface components {
domain: string;
image_src: string;
raw_image_src: string;
time_to_read?: number;
};
LegacySettings: {
spocsPerNewTabs?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ query NewTabRecommendations($locale: String!, $region: String, $count: Int) {
publisher
title
url
timeToRead
}
}
}
Expand Down
46 changes: 35 additions & 11 deletions src/graphql-proxy/recommendations/__mocks__/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import common from '../../__mocks__/common';

import { NewTabRecommendationsQuery } from '../../../generated/graphql/types';
import { RecommendationsParameters } from '../recommendations';
import { GraphRecommendation } from '../../../api/desktop/recommendations/response';

/**
* faker locales do not match our own, map ours to faker locales
Expand All @@ -25,22 +26,45 @@ const fakerLocales = {
it: 'it',
};

/**
*
* @param hasTimeToRead If true, timeToRead is set to [1, 9], otherwise it's undefined.
*/
const fakeRecommendation = (hasTimeToRead = true): GraphRecommendation => {
const recommendationWithoutTimeToRead: GraphRecommendation = {
__typename: 'CorpusRecommendation',
tileId: faker.datatype.number(),
corpusItem: {
excerpt: common.itemExcerpt(),
imageUrl: common.itemUrl(),
publisher: common.itemDomain(),
title: common.itemTitle(),
url: common.itemUrl(),
},
};

if (!hasTimeToRead) {
return recommendationWithoutTimeToRead;
}

return {
...recommendationWithoutTimeToRead,
corpusItem: {
...recommendationWithoutTimeToRead.corpusItem,
timeToRead: faker.datatype.number({ min: 1, max: 9 }),
},
};
};

This comment was marked as resolved.

const fakeRecommendations = (
count
): NewTabRecommendationsQuery['newTabSlate']['recommendations'] => {
return Array(count)
.fill(0)
.map(() => ({
__typename: 'CorpusRecommendation',
tileId: faker.datatype.number(),
corpusItem: {
excerpt: common.itemExcerpt(),
imageUrl: common.itemUrl(),
publisher: common.itemDomain(),
title: common.itemTitle(),
url: common.itemUrl(),
},
}));
.map((value, index) =>
// Add timeToRead only for even numbered recommendations.
fakeRecommendation(index % 2 === 0)
);
};

const recommendations = async ({
Expand Down