Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notification system #4149

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open

Notification system #4149

wants to merge 30 commits into from

Conversation

gonzaloriestra
Copy link
Contributor

@gonzaloriestra gonzaloriestra commented Jul 3, 2024

WHY are these changes introduced?

We want to notify CLI users about deprecations, new features or events

WHAT is this pull request doing?

Adds two commands:

  • notifications generate: shows several prompts asking for the notification content, type and filters, and adds it to a local notifications.json file.
  • notifications list: shows the current notifications from the local notifications.json

Then, before each command we check the remote notifications.json from this repository and show the required ones. The file is cached for 24h.

Demo: https://hackdays.shopify.io/projects/18981

How to test your changes?

  • p shopify notifications generate
  • p shopify notifications list

Post-release steps

Merge the internal doc: https://github.com/Shopify/vault-pages/pull/7111

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

@gonzaloriestra gonzaloriestra added the includes-post-release-steps PRs including this label require additional steps after releasing label Jul 3, 2024
Copy link
Contributor

github-actions bot commented Jul 3, 2024

Thanks for your contribution!

Depending on what you are working on, you may want to request a review from a Shopify team:

  • Themes: @shopify/advanced-edits
  • UI extensions: @shopify/ui-extensions-cli
    • Checkout UI extensions: @shopify/checkout-ui-extensions-api-stewardship
  • Hydrogen: @shopify/hydrogen
  • Other: @shopify/app-management

import {CLI_KIT_VERSION} from '../common/version.js'
import {NotificationsKey, cacheRetrieveOrRepopulate, getCache, setCache} from '../../private/node/conf-store.js'

const URL = 'https://raw.githubusercontent.com/Shopify/cli/notifications/notifications.json'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have to change this URL before merging to:

Suggested change
const URL = 'https://raw.githubusercontent.com/Shopify/cli/notifications/notifications.json'
const URL = 'https://raw.githubusercontent.com/Shopify/cli/main/notifications.json'

Copy link
Contributor

github-actions bot commented Jul 3, 2024

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements
72.4% (-0.16% 🔻)
7542/10417
🟡 Branches
68.99% (-0.21% 🔻)
3716/5386
🟡 Functions
71.61% (+0.14% 🔼)
1998/2790
🟡 Lines
72.79% (-0.11% 🔻)
7123/9786
Show new covered files 🐣
St.
File Statements Branches Functions Lines
🔴
... / generate.ts
0% 100% 0% 0%
🔴
... / list.ts
0% 100% 0% 0%
🔴
... / notifications.ts
0% 0% 0% 0%
🟢
... / global-context.ts
100% 100% 100% 100%
🟡
... / notifications-system.ts
64.47% 63.16% 83.33% 72.58%
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢
... / conf-store.ts
100%
70.59% (-6.33% 🔻)
100% 100%

Test suite run success

1735 tests passing in 797 suites.

Report generated by 🧪jest coverage report action from 3f53781

Copy link
Contributor

github-actions bot commented Jul 3, 2024

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/node/global-context.d.ts
export interface GlobalContext {
    currentCommandId: string;
    [key: string]: string;
}
/**
 * Get the current command ID.
 *
 * @returns Current command ID.
 */
export declare function getCurrentCommandId(): string;
/**
 * Set the current command ID.
 *
 * @param commandId - Command ID.
 */
export declare function setCurrentCommandId(commandId: string): void;
packages/cli-kit/dist/public/node/notifications-system.d.ts
export interface Notifications {
    notifications: Notification[];
}
export interface Notification {
    id: string;
    message: string;
    cta?: {
        label: string;
        url: string;
    };
    type: 'info' | 'warning' | 'error';
    title?: string;
    minVersion?: string;
    maxVersion?: string;
    minDate?: string;
    maxDate?: string;
    commands?: string[];
    surface?: 'app' | 'theme' | 'hydrogen' | string;
    frequency?: 'always' | 'once' | 'once_a_day' | 'once_a_week';
}
/**
 * Shows notifications to the user if they meet the criteria specified in the notifications.json file.
 *
 * @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
 * @returns - A promise that resolves when the notifications have been shown.
 */
export declare function showNotificationsIfNeeded(currentSurfaces?: string[]): Promise<void>;
/**
 * Get notifications list from cache or fetch it if not present.
 *
 * @returns A Notifications object.
 */
export declare function getNotifications(): Promise<Notifications>;
/**
 * Filters notifications based on the version of the CLI.
 *
 * @param notifications - The notifications to filter.
 * @param commandId - The command ID to filter by.
 * @param currentSurfaces - The surfaces present in the current project (usually for app extensions).
 * @param today - The current date.
 * @param currentVersion - The current version of the CLI.
 * @returns - The filtered notifications.
 */
export declare function filterNotifications(notifications: Notification[], commandId: string, currentSurfaces?: string[], today?: Date, currentVersion?: string): Notification[];
/**
 * Returns a string with the filters from a notification, one by line.
 *
 * @param notification - The notification to get the filters from.
 * @returns A string with human-readable filters from the notification.
 */
export declare function stringifyFilters(notification: Notification): string;
/**
 * Reads the notifications from the local file.
 *
 * @returns A Notifications object.
 */
export declare function getLocalNotifications(): Promise<Notifications>;

Existing type declarations

packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -5,9 +5,13 @@ interface CacheValue<T> {
 }
 export type IntrospectionUrlKey = ;
 export type PackageVersionKey = ;
+export type NotificationsKey = ;
+type NotificationKey = ;
 interface Cache {
     [introspectionUrlKey: IntrospectionUrlKey]: CacheValue<string>;
     [packageVersionKey: PackageVersionKey]: CacheValue<string>;
+    [notifications: NotificationsKey]: CacheValue<string>;
+    [notification: NotificationKey]: CacheValue<string>;
 }
 export interface ConfSchema {
     sessionStore: string;
@@ -40,4 +44,6 @@ type CacheValueForKey<TKey extends keyof Cache> = NonNullable<Cache[TKey]>['valu
  * @returns The value from the cache or the result of the function.
  */
 export declare function cacheRetrieveOrRepopulate(key: keyof Cache, fn: () => Promise<CacheValueForKey<typeof key>>, timeout?: number, config?: LocalStorage<ConfSchema>): Promise<CacheValueForKey<typeof key>>;
+export declare function getCache(key: keyof Cache, config?: LocalStorage<ConfSchema>): CacheValue<string> | undefined;
+export declare function setCache(key: keyof Cache, value: string, config?: LocalStorage<ConfSchema>): void;
 export {};
\ No newline at end of file

@gonzaloriestra gonzaloriestra marked this pull request as ready for review July 3, 2024 08:36
Copy link
Contributor

github-actions bot commented Jul 3, 2024

We detected some changes at either packages/*/src or packages/cli-kit/assets/cli-ruby/** and there are no updates in the .changeset.
If the changes are user-facing, run "pnpm changeset add" to track your changes and include them in the next release CHANGELOG.

@isaacroldan
Copy link
Contributor

/snapit

@@ -0,0 +1,36 @@
export interface GlobalContext {
currentCommandId: string
[key: string]: string
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this for? It basically makes GlobalContext close to untyped -- I haven't seen a use of it. Would drop it if not used.

Copy link
Contributor

Choose a reason for hiding this comment

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

I added it initially to support anything, but we don't need it anymore if we have specific types for each property stored in the context. We should remove it.

/**
* Fetch notifications from GitHub.
*/
async function fetchNotifications(): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should look into wrapping this in a timeout. Like, if it takes longer than Xms to load, we stop waiting and move on. Given that the command is paused whilst this is happening.

notifications: Notification[]
}

export interface Notification {
Copy link
Contributor

Choose a reason for hiding this comment

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

The typing here might not match the format used in the notification file -- e.g. if that file evolves but the CLI reading from it is an old one. I think it'd be helpful to defend against that -- whilst we're the authors of the notification file, we're still decoupled from it. I think I would swap this for a Zod schema, and parse the file using that. Then, if parsing fails because it doesn't match what this CLI's schema is expecting we can skip the notifications and move on. Obviously we would also try not to break the file as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good idea 👌

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are catching errors here, so if the parsing fails, it already moves on. Any other advantage of using Zod here?

Copy link
Contributor

Choose a reason for hiding this comment

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

That only catches errors -- something that literally ends up in a thrown error -- if the format changes you might have a scenario where no error is thrown but the behaviour is wrong

* @param notifications - The notifications to render.
*/
async function renderNotifications(notifications: Notification[]) {
notifications.forEach((notification) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is going to be quite noisy if there are multiple notifications matching on any given command run -- each section having it's own box. I'm not sure what the best alternative is. Or, how likely it is to happen. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know. We thought about it and decided to show all. What if there are two important notifications? Also, we are in control of the messages, so we should be careful about not bothering too much and try to minimize multiple ones.

However, we can improve it in the future and maybe add a priority field to only show one or two.

* Fetch notifications from GitHub.
*/
async function fetchNotifications(): Promise<string> {
const response = await fetch(URL)
Copy link
Contributor

Choose a reason for hiding this comment

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

There's an analytics helper to separate out network time from time with the command being active. We should wrap this up in that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
includes-post-release-steps PRs including this label require additional steps after releasing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants