Skip to content

Commit 6c9b625

Browse files
committed
chore: Persist and filter features with timestamps
1 parent 82d6251 commit 6c9b625

File tree

4 files changed

+66
-30
lines changed

4 files changed

+66
-30
lines changed

pages/feature-notifications/feature-prompt.page.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,23 @@ import labels from '../app-layout/utils/labels';
1515
import * as toolsContent from '../app-layout/utils/tools-content';
1616
import ScreenshotArea from '../utils/screenshot-area';
1717

18-
const readFeaturesStorage: Record<string, Array<string>> = {
19-
'feature-notifications': [],
18+
const readFeaturesStorage: Record<string, Record<string, string>> = {
19+
'feature-notifications': {},
2020
};
2121

2222
setPersistenceFunctionsForTesting({
2323
persistSeenFeatureNotifications: async function (persistenceConfig, value) {
24-
const readFeatures = readFeaturesStorage[persistenceConfig.uniqueKey] ?? [];
2524
const result = await new Promise<void>(resolve =>
2625
setTimeout(() => {
27-
readFeaturesStorage[persistenceConfig.uniqueKey] = [...readFeatures, ...value];
26+
readFeaturesStorage[persistenceConfig.uniqueKey] = value;
2827
resolve();
2928
}, 150)
3029
);
3130
return result;
3231
},
3332
retrieveSeenFeatureNotifications: async function (persistenceConfig) {
34-
const result = await new Promise<Array<string>>(resolve =>
35-
setTimeout(() => resolve(readFeaturesStorage[persistenceConfig.uniqueKey] ?? []), 150)
33+
const result = await new Promise<Record<string, string>>(resolve =>
34+
setTimeout(() => resolve(readFeaturesStorage[persistenceConfig.uniqueKey] ?? {}), 150)
3635
);
3736
return result;
3837
},

src/app-layout/__tests__/runtime-feature-notifications.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const mockPersistSeenFeatureNotifications = jest.mocked(persistSeenFeatureNotifi
6767
beforeEach(() => {
6868
awsuiWidgetInternal.clearInitialMessages();
6969
jest.resetAllMocks();
70-
mockRetrieveSeenFeatureNotifications.mockResolvedValue([]);
70+
mockRetrieveSeenFeatureNotifications.mockResolvedValue({});
7171
mockPersistSeenFeatureNotifications.mockResolvedValue();
7272

7373
// Mock current date for consistent filtering
@@ -164,7 +164,7 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, () => {
164164
});
165165

166166
test('shows feature prompt for a latest unseen features', async () => {
167-
mockRetrieveSeenFeatureNotifications.mockResolvedValue(['feature-1']);
167+
mockRetrieveSeenFeatureNotifications.mockResolvedValue({ 'feature-1': mockDate2025.toString() });
168168
awsuiWidgetPlugins.registerFeatureNotifications(featureNotificationsDefaults);
169169
const { container } = await renderComponent(<AppLayout />);
170170

@@ -199,7 +199,11 @@ describeEachAppLayout({ themes: ['refresh-toolbar'] }, () => {
199199
});
200200

201201
test('should not show feature prompt if all feature are seen', async () => {
202-
mockRetrieveSeenFeatureNotifications.mockResolvedValue(['feature-1', 'feature-2', 'feature-old']);
202+
mockRetrieveSeenFeatureNotifications.mockResolvedValue({
203+
'feature-1': mockDate2025.toString(),
204+
'feature-2': mockDate2024.toString(),
205+
'feature-old': mockDateOld.toString(),
206+
});
203207
awsuiWidgetPlugins.registerFeatureNotifications({ ...featureNotificationsDefaults, suppressFeaturePrompt: true });
204208
const { container } = await renderComponent(<AppLayout />);
205209

src/app-layout/visual-refresh-toolbar/state/use-feature-notifications.tsx

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import React, { RefObject, useEffect, useRef, useState } from 'react';
44

55
import FeaturePrompt, { FeaturePromptProps } from '../../../internal/do-not-use/feature-prompt';
6+
import { metrics } from '../../../internal/metrics';
67
import { persistSeenFeatureNotifications, retrieveSeenFeatureNotifications } from '../../../internal/persistence';
78
import awsuiPlugins from '../../../internal/plugins';
89
import { Feature, FeatureNotificationsPayload, WidgetMessage } from '../../../internal/plugins/widget/interfaces';
910
import RuntimeFeaturesNotificationDrawer, { RuntimeContentPart } from '../drawer/feature-notifications-drawer-content';
11+
1012
interface UseFeatureNotificationsProps {
1113
activeDrawersIds: Array<string>;
1214
}
@@ -34,30 +36,64 @@ function subtractDaysFromDate(currentDate: Date, daysToSubtract: number) {
3436
return pastDate;
3537
}
3638

39+
function filterOutdatedFeatures(features: Record<string, string>): Record<string, string> {
40+
const cutoffDate = subtractDaysFromDate(new Date(), 180);
41+
42+
return Object.keys(features).reduce((acc, key) => {
43+
const featureDate = new Date(features[key]);
44+
45+
if (featureDate && featureDate >= cutoffDate) {
46+
return {
47+
...acc,
48+
[key]: features[key],
49+
};
50+
}
51+
52+
return acc;
53+
}, {});
54+
}
55+
3756
export function useFeatureNotifications({ activeDrawersIds }: UseFeatureNotificationsProps) {
3857
const [markAllAsRead, setMarkAllAsRead] = useState(false);
3958
const [featurePromptDismissed, setFeaturePromptDismissed] = useState(false);
4059
const [featureNotificationsData, setFeatureNotificationsData] = useState<FeatureNotificationsPayload<unknown> | null>(
4160
null
4261
);
43-
const [seenFeatureIds, setSeenFeatureIds] = useState<Set<string>>(new Set());
62+
const [seenFeatures, setSeenFeatures] = useState<Record<string, string>>({});
4463
const featurePromptRef = useRef<FeaturePromptProps.Ref>(null);
4564

4665
useEffect(() => {
4766
if (!featureNotificationsData || markAllAsRead) {
4867
return;
4968
}
50-
const id = featureNotificationsData.id;
51-
if (activeDrawersIds.includes(id) && !markAllAsRead) {
52-
const allFeaturesIds = [...seenFeatureIds, ...featureNotificationsData.features.map(feature => feature.id)];
53-
const uniqueAllFeatureIds = [...new Set(allFeaturesIds)];
54-
persistSeenFeatureNotifications(persistenceConfig, uniqueAllFeatureIds).then(() => {
55-
awsuiPlugins.appLayout.updateDrawer({ id, badge: false });
56-
setMarkAllAsRead(true);
57-
});
69+
try {
70+
const id = featureNotificationsData?.id;
71+
if (activeDrawersIds.includes(id) && !markAllAsRead) {
72+
const featuresMap = featureNotificationsData.features.reduce((acc, feature) => {
73+
return {
74+
...acc,
75+
[feature.id]: feature.releaseDate.toString(),
76+
};
77+
}, {});
78+
const filteredSeenFeaturesMap = filterOutdatedFeatures(seenFeatures);
79+
const allFeaturesMap = { ...featuresMap, ...filteredSeenFeaturesMap };
80+
persistSeenFeatureNotifications(persistenceConfig, allFeaturesMap).then(() => {
81+
awsuiPlugins.appLayout.updateDrawer({ id, badge: false });
82+
setMarkAllAsRead(true);
83+
});
84+
}
5885
return;
86+
} catch (error) {
87+
let message = '';
88+
if (error instanceof Error) {
89+
message = error?.message ?? '';
90+
}
91+
metrics.sendOpsMetricObject('awsui-widget-feature-notifications-error', {
92+
id: featureNotificationsData?.id,
93+
message,
94+
});
5995
}
60-
}, [featureNotificationsData, activeDrawersIds, markAllAsRead, featurePromptDismissed, seenFeatureIds]);
96+
}, [featureNotificationsData, activeDrawersIds, markAllAsRead, featurePromptDismissed, seenFeatures]);
6197

6298
const defaultFeaturesFilter = (feature: Feature<unknown>) => {
6399
return feature.releaseDate >= subtractDaysFromDate(new Date(), 90);
@@ -103,9 +139,8 @@ export function useFeatureNotifications({ activeDrawersIds }: UseFeatureNotifica
103139
});
104140

105141
retrieveSeenFeatureNotifications(persistenceConfig).then(seenFeatureNotifications => {
106-
const seenFeatureNotificationsSet = new Set(seenFeatureNotifications);
107-
setSeenFeatureIds(seenFeatureNotificationsSet);
108-
const hasUnseenFeatures = features.some(feature => !seenFeatureNotificationsSet.has(feature.id));
142+
setSeenFeatures(seenFeatureNotifications);
143+
const hasUnseenFeatures = features.some(feature => !seenFeatureNotifications[feature.id]);
109144
if (hasUnseenFeatures) {
110145
if (!payload.suppressFeaturePrompt && !featurePromptDismissed) {
111146
featurePromptRef.current?.show();
@@ -117,7 +152,6 @@ export function useFeatureNotifications({ activeDrawersIds }: UseFeatureNotifica
117152
}
118153

119154
if (event.type === 'showFeaturePromptIfPossible') {
120-
console.log('showFeaturePromptIfPossible markAllAsRead: ', markAllAsRead);
121155
if (markAllAsRead) {
122156
return;
123157
}
@@ -134,12 +168,11 @@ export function useFeatureNotifications({ activeDrawersIds }: UseFeatureNotifica
134168
// Features array is already sorted in reverse chronological order (most recent first)
135169
// Find the first feature that hasn't been seen
136170
for (const feature of featureNotificationsData.features) {
137-
if (!seenFeatureIds.has(feature.id)) {
171+
if (!seenFeatures[feature.id]) {
138172
return feature;
139173
}
140174
}
141175

142-
// No unseen features found
143176
return null;
144177
}
145178

src/internal/persistence/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ interface PersistenceFunction {
1313
retrieveAlertDismiss?: (persistenceConfig: AlertProps.PersistenceConfig) => Promise<boolean>;
1414
persistSeenFeatureNotifications?: (
1515
persistenceConfig: FeaturePromptProps.PersistenceConfig,
16-
value: Array<string>
16+
value: Record<string, string>
1717
) => Promise<void>;
1818
retrieveSeenFeatureNotifications?: (
1919
persistenceConfig: FeaturePromptProps.PersistenceConfig
20-
) => Promise<Array<string>>;
20+
) => Promise<Record<string, string>>;
2121
}
2222

2323
export function setPersistenceFunctionsForTesting(functions: PersistenceFunction) {
@@ -78,7 +78,7 @@ export let persistSeenFeatureNotifications = async function (
7878
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7979
persistenceConfig: FeaturePromptProps.PersistenceConfig,
8080
// eslint-disable-next-line @typescript-eslint/no-unused-vars
81-
value: Array<string>
81+
value: Record<string, string>
8282
): Promise<void> {
8383
return Promise.resolve();
8484
};
@@ -87,6 +87,6 @@ export let persistSeenFeatureNotifications = async function (
8787
export let retrieveSeenFeatureNotifications = async function (
8888
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8989
persistenceConfig: FeaturePromptProps.PersistenceConfig
90-
): Promise<Array<string>> {
91-
return Promise.resolve([]);
90+
): Promise<Record<string, string>> {
91+
return Promise.resolve({});
9292
};

0 commit comments

Comments
 (0)