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
327 changes: 250 additions & 77 deletions __tests__/cron/channelHighlights.ts

Large diffs are not rendered by default.

414 changes: 414 additions & 0 deletions src/common/channelHighlight/canonical.ts

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/common/channelHighlight/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { HighlightPost } from './types';
import { buildStoryFamilies } from './storyFamilies';

type HighlightChannelResolverInput = {
posts: HighlightPost[];
relations: { postId: string; relatedPostId: string }[];
fallbackPostIds: Map<string, string>;
};

export type HighlightChannelResolver = (postId: string) => string[];

const getPostChannels = (post: HighlightPost | undefined): string[] => {
const contentMeta = post?.contentMeta as { channels?: unknown } | undefined;
const channels = contentMeta?.channels;
if (!Array.isArray(channels)) {
return [];
}

return [
...new Set(
channels.filter(
(channel): channel is string => typeof channel === 'string',
),
),
].sort();
};

export const createHighlightChannelResolver = ({
posts,
relations,
fallbackPostIds,
}: HighlightChannelResolverInput): HighlightChannelResolver => {
const postsById = new Map(posts.map((post) => [post.id, post]));
const shareToUnderlying = new Map(
[...fallbackPostIds].map(([underlying, share]) => [share, underlying]),
);
const storyFamilies = buildStoryFamilies({ relations });

return (postId) => {
const underlyingPostId = shareToUnderlying.get(postId) || postId;
const storyPostIds = storyFamilies.getFamilyPostIds(underlyingPostId);
const channels = new Set<string>();

for (const storyPostId of storyPostIds) {
for (const channel of getPostChannels(postsById.get(storyPostId))) {
channels.add(channel);
}
}

return [...channels].sort();
};
};
8 changes: 8 additions & 0 deletions src/common/channelHighlight/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const GLOBAL_HIGHLIGHT_CHANNEL = 'global';

export const DEFAULT_CANDIDATE_HORIZON_HOURS = 168;

export const DEFAULT_MAX_ITEMS = 20;

export const GLOBAL_TARGET_AUDIENCE =
'software engineers and engineering leaders who want to stay current on meaningful developments that affect how modern software is built, shipped, operated, and grown';
22 changes: 9 additions & 13 deletions src/common/channelHighlight/decisions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,20 @@ export const compareSnapshots = ({
const changed =
baseline.map(toItemSignature).join('||') !==
internal.map(toItemSignature).join('||');
const addedPostIds = [...internalByPostId.keys()].filter(
(postId) => !baselineByPostId.has(postId),
);
const removedPostIds = [...baselineByPostId.keys()].filter(
(postId) => !internalByPostId.has(postId),
);

return {
changed,
baselineCount: baseline.length,
internalCount: internal.length,
overlapCount: overlap.length,
addedPostIds: [...internalByPostId.keys()].filter(
(postId) => !baselineByPostId.has(postId),
),
removedPostIds: [...baselineByPostId.keys()].filter(
(postId) => !internalByPostId.has(postId),
),
churnCount:
[...internalByPostId.keys()].filter(
(postId) => !baselineByPostId.has(postId),
).length +
[...baselineByPostId.keys()].filter(
(postId) => !internalByPostId.has(postId),
).length,
addedPostIds,
removedPostIds,
churnCount: addedPostIds.length + removedPostIds.length,
};
};
17 changes: 7 additions & 10 deletions src/common/channelHighlight/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import {
EvaluateChannelHighlightsRequest as BragiEvaluateChannelHighlightsRequest,
} from '@dailydotdev/schema';
import { getBragiClient } from '../../integrations/bragi/clients';
import { GLOBAL_HIGHLIGHT_CHANNEL, GLOBAL_TARGET_AUDIENCE } from './constants';
import type { HighlightCandidate, HighlightItem } from './types';

export type EvaluateChannelHighlightsRequest = {
channel: string;
targetAudience: string;
export type EvaluateHighlightsRequest = {
maxItems: number;
currentHighlights: HighlightItem[];
newCandidates: HighlightCandidate[];
Expand All @@ -23,7 +22,7 @@ export type EvaluatedHighlightItem = {
reason: string;
};

export type EvaluateChannelHighlightsResponse = {
export type EvaluateHighlightsResponse = {
items: EvaluatedHighlightItem[];
};

Expand Down Expand Up @@ -95,13 +94,11 @@ const toCandidate = (
relatedItemsCount: candidate.relatedItemsCount,
});

export const evaluateChannelHighlights = async ({
channel,
targetAudience,
export const evaluateHighlights = async ({
maxItems,
currentHighlights,
newCandidates,
}: EvaluateChannelHighlightsRequest): Promise<EvaluateChannelHighlightsResponse> => {
}: EvaluateHighlightsRequest): Promise<EvaluateHighlightsResponse> => {
if (!newCandidates.length) {
return {
items: [],
Expand All @@ -110,8 +107,8 @@ export const evaluateChannelHighlights = async ({

const bragiClient = getBragiClient();
const request = new BragiEvaluateChannelHighlightsRequest({
channel,
targetAudience,
channel: GLOBAL_HIGHLIGHT_CHANNEL,
targetAudience: GLOBAL_TARGET_AUDIENCE,
maxItems,
currentHighlights: currentHighlights.map(toCurrentHighlight),
newCandidates: newCandidates.map(toCandidate),
Expand Down
Loading
Loading