Repository: refined-github/refined-github
Pull request #7899 (2024-10-14 09:19:31)

Improve console output for features that require a token
- Mentioned in https://github.com/refined-github/refined-github/issues/6000#issuecomment-2185499249
- Follows https://github.com/refined-github/refined-github/pull/7798
- Related to https://github.com/refined-github/refined-github/pull/7825


This surfaces the errors earlier in features that use `observe`/`delegate`, even if the callback hasn't been called yet


More error-related work:

- https://github.com/refined-github/refined-github/issues/6096


fregante: # Before

<img width="814" alt="Screenshot 7" src="https://github.com/user-attachments/assets/0dc2040f-66dd-49f8-a03d-9a872ba941f6">

# After

<img width="814" alt="Screenshot 8" src="https://github.com/user-attachments/assets/b39ac822-da29-4aa5-acbe-bbf51df6507a">


Patch:
diff --git a/source/feature-manager.tsx b/source/feature-manager.tsx
index b261d993..7d948773 100644
--- a/source/feature-manager.tsx
+++ b/source/feature-manager.tsx
@@ -79,7 +79,7 @@ function logError(url: string, error: unknown): void {
 	}
 
 	if (message.includes('token')) {
-		console.log('ℹ️', id, '→', message);
+		console.log('ℹ️', message, '→', id);
 		return;
 	}
 
diff --git a/source/features/bugs-tab.tsx b/source/features/bugs-tab.tsx
index 04fa9acd..01e0d4b2 100644
--- a/source/features/bugs-tab.tsx
+++ b/source/features/bugs-tab.tsx
@@ -13,6 +13,7 @@ import abbreviateNumber from '../helpers/abbreviate-number.js';
 import {highlightTab, unhighlightTab} from '../helpers/dom-utils.js';
 import isBugLabel from '../github-helpers/bugs-label.js';
 import CountBugs from './bugs-tab.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type Bugs = {
 	label: string;
@@ -156,6 +157,8 @@ async function updateBugsTagHighlighting(): Promise<void | false> {
 }
 
 async function init(): Promise<void | false> {
+	await expectToken();
+
 	if (!elementExists('.rgh-bugs-tab')) {
 		await addBugsTab();
 	}
diff --git a/source/features/ci-link.tsx b/source/features/ci-link.tsx
index daf3fa95..483437f7 100644
--- a/source/features/ci-link.tsx
+++ b/source/features/ci-link.tsx
@@ -7,6 +7,7 @@ import api from '../github-helpers/api.js';
 import {buildRepoURL} from '../github-helpers/index.js';
 import observe from '../helpers/selector-observer.js';
 import getChecks from './ci-link.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function getCommitWithChecks(): Promise<string | undefined> {
 	const {repository} = await api.v4(getChecks);
@@ -44,16 +45,14 @@ async function add(anchor: HTMLElement): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	observe([
 		// Desktop
 		'.AppHeader-context-item:not([data-hovercard-type])',
 
 		// Mobile. `> *:first-child` avoids finding our own element
 		'.AppHeader-context-compact-mainItem > span:first-child',
-
-		// Old selector: `.avatar` excludes "Global navigation update"
-		// Repo title (aware of forks and private repos)
-		'[itemprop="name"]:not(.avatar ~ [itemprop])',
 	], add, {signal});
 }
 
diff --git a/source/features/clean-conversation-filters.tsx b/source/features/clean-conversation-filters.tsx
index 2cdf8833..5d757b03 100644
--- a/source/features/clean-conversation-filters.tsx
+++ b/source/features/clean-conversation-filters.tsx
@@ -7,7 +7,7 @@ import features from '../feature-manager.js';
 import {cacheByRepo} from '../github-helpers/index.js';
 import HasAnyProjects from './clean-conversation-filters.gql';
 import api from '../github-helpers/api.js';
-import {expectTokenScope} from '../github-helpers/github-token.js';
+import {expectToken, expectTokenScope} from '../github-helpers/github-token.js';
 import observe from '../helpers/selector-observer.js';
 
 const hasAnyProjects = new CachedFunction('has-projects', {
@@ -69,7 +69,8 @@ async function hide(container: HTMLElement): Promise<void> {
 	void hideProjects(container);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe([
 		'#js-issues-toolbar', // TODO: Remove after March 2025
 		'[data-testid="list-view-metadata"]',
diff --git a/source/features/clean-conversation-headers.tsx b/source/features/clean-conversation-headers.tsx
index 3ad09829..08bc8841 100644
--- a/source/features/clean-conversation-headers.tsx
+++ b/source/features/clean-conversation-headers.tsx
@@ -8,6 +8,7 @@ import * as pageDetect from 'github-url-detection';
 import features from '../feature-manager.js';
 import getDefaultBranch from '../github-helpers/get-default-branch.js';
 import observe from '../helpers/selector-observer.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function cleanIssueHeader(byline: HTMLElement): Promise<void> {
 	byline.classList.add('rgh-clean-conversation-headers', 'rgh-clean-conversation-headers-hide-author');
@@ -52,6 +53,8 @@ async function cleanPrHeader(byline: HTMLElement): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	const cleanConversationHeader = pageDetect.isIssue() ? cleanIssueHeader : cleanPrHeader;
 	observe([
 		'.gh-header-meta > .flex-auto', // Real
diff --git a/source/features/clear-pr-merge-commit-message.tsx b/source/features/clear-pr-merge-commit-message.tsx
index 9b50aaad..2cfb4144 100644
--- a/source/features/clear-pr-merge-commit-message.tsx
+++ b/source/features/clear-pr-merge-commit-message.tsx
@@ -8,6 +8,7 @@ import getDefaultBranch from '../github-helpers/get-default-branch.js';
 import cleanCommitMessage from '../helpers/clean-commit-message.js';
 import {userHasPushAccess} from '../github-helpers/get-user-permission.js';
 import observe from '../helpers/selector-observer.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const isPrAgainstDefaultBranch = async (): Promise<boolean> => getBranches().base.branch === await getDefaultBranch();
 
@@ -38,7 +39,8 @@ async function clear(messageField: HTMLTextAreaElement): Promise<void | false> {
 	);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('textarea#merge_message_field', clear, {signal});
 }
 
diff --git a/source/features/comments-time-machine-links.tsx b/source/features/comments-time-machine-links.tsx
index f5ad2e83..16b26f29 100644
--- a/source/features/comments-time-machine-links.tsx
+++ b/source/features/comments-time-machine-links.tsx
@@ -12,6 +12,7 @@ import {buildRepoURL, isPermalink} from '../github-helpers/index.js';
 import {saveOriginalHref} from './sort-conversations-by-update-time.js';
 import observe from '../helpers/selector-observer.js';
 import GetCommitAtDate from './comments-time-machine-links.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function updateURLtoDatedSha(url: GitHubFileURL, date: string): Promise<void> {
 	const {repository} = await api.v4(GetCommitAtDate, {variables: {date, branch: url.branch}});
@@ -98,7 +99,9 @@ function addDropdownLink(menu: HTMLElement, timestamp: string): void {
 	);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	observe('.timeline-comment-actions > details:last-child', menu => {
 		if (menu.closest('.js-pending-review-comment')) {
 			return;
diff --git a/source/features/conflict-marker.tsx b/source/features/conflict-marker.tsx
index 4baf37d0..0bc7c2f8 100644
--- a/source/features/conflict-marker.tsx
+++ b/source/features/conflict-marker.tsx
@@ -8,6 +8,7 @@ import features from '../feature-manager.js';
 import api from '../github-helpers/api.js';
 import observe from '../helpers/selector-observer.js';
 import {openPrsListLink} from '../github-helpers/selectors.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function addIcon(links: HTMLAnchorElement[]): Promise<void> {
 	const prConfigs = links.map(link => {
@@ -48,7 +49,8 @@ async function addIcon(links: HTMLAnchorElement[]): Promise<void> {
 	}
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe(openPrsListLink, batchedFunction(addIcon, {delay: 100}), {signal});
 }
 
diff --git a/source/features/deep-reblame.tsx b/source/features/deep-reblame.tsx
index bd62ce14..b716b5f3 100644
--- a/source/features/deep-reblame.tsx
+++ b/source/features/deep-reblame.tsx
@@ -14,6 +14,7 @@ import looseParseInt from '../helpers/loose-parse-int.js';
 import observe from '../helpers/selector-observer.js';
 import GetPullRequestBlameCommit from './deep-reblame.gql';
 import {multilineAriaLabel} from '../github-helpers/index.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const getPullRequestBlameCommit = mem(async (commit: string, prNumbers: number[], currentFilename: string): Promise<string> => {
 	const {repository} = await api.v4(GetPullRequestBlameCommit, {
@@ -86,7 +87,9 @@ function addButton(hunk: HTMLElement): void {
 	}
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	delegate('.rgh-deep-reblame', 'click', redirectToBlameCommit, {signal});
 	observe('.react-blame-for-range:has([data-hovercard-type="pull_request"])', addButton, {signal});
 }
diff --git a/source/features/default-branch-button.tsx b/source/features/default-branch-button.tsx
index d664df1c..275ae123 100644
--- a/source/features/default-branch-button.tsx
+++ b/source/features/default-branch-button.tsx
@@ -13,6 +13,7 @@ import observe from '../helpers/selector-observer.js';
 import {branchSelector} from '../github-helpers/selectors.js';
 import isDefaultBranch from '../github-helpers/is-default-branch.js';
 import {fixFileHeaderOverlap, isRepoCommitListRoot} from '../github-helpers/index.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const getUrl = memoize(async (currentUrl: string): Promise<string> => {
 	const defaultUrl = new GitHubFileURL(currentUrl);
@@ -77,7 +78,8 @@ async function add(branchSelector: HTMLElement): Promise<void> {
 	wrapButtons([defaultLink, selectorWrapper]);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe(branchSelector, add, {signal});
 }
 
diff --git a/source/features/github-actions-indicators.tsx b/source/features/github-actions-indicators.tsx
index 4d78b905..9b4d84cc 100644
--- a/source/features/github-actions-indicators.tsx
+++ b/source/features/github-actions-indicators.tsx
@@ -10,6 +10,7 @@ import api from '../github-helpers/api.js';
 import {cacheByRepo} from '../github-helpers/index.js';
 import observe from '../helpers/selector-observer.js';
 import GetWorkflows from './github-actions-indicators.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type Workflow = {
 	name: string;
@@ -133,6 +134,7 @@ async function addIndicators(workflowListItem: HTMLAnchorElement): Promise<void>
 }
 
 async function init(signal: AbortSignal): Promise<false | void> {
+	await expectToken();
 	observe('a.ActionListContent', addIndicators, {signal});
 }
 
diff --git a/source/features/highlight-non-default-base-branch.tsx b/source/features/highlight-non-default-base-branch.tsx
index a5341a13..788c9026 100644
--- a/source/features/highlight-non-default-base-branch.tsx
+++ b/source/features/highlight-non-default-base-branch.tsx
@@ -8,6 +8,7 @@ import api from '../github-helpers/api.js';
 import {buildRepoURL} from '../github-helpers/index.js';
 import getDefaultBranch from '../github-helpers/get-default-branch.js';
 import observe from '../helpers/selector-observer.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type BranchInfo = {
 	baseRef: string;
@@ -70,6 +71,7 @@ async function add(prLinks: HTMLElement[]): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<false | void> {
+	await expectToken();
 	observe('.js-issue-row .js-navigation-open[data-hovercard-type="pull_request"]', batchedFunction(add, {delay: 100}), {signal});
 }
 
diff --git a/source/features/list-prs-for-file.tsx b/source/features/list-prs-for-file.tsx
index 49a3e780..c3aacde1 100644
--- a/source/features/list-prs-for-file.tsx
+++ b/source/features/list-prs-for-file.tsx
@@ -12,6 +12,7 @@ import {buildRepoURL, cacheByRepo} from '../github-helpers/index.js';
 import GitHubFileURL from '../github-helpers/github-file-url.js';
 import observe from '../helpers/selector-observer.js';
 import listPrsForFileQuery from './list-prs-for-file.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 function getPRUrl(prNumber: number): string {
 	// https://caniuse.com/url-scroll-to-text-fragment
@@ -132,7 +133,9 @@ async function add(anchor: HTMLElement): Promise<false | void> {
 	}
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	observe([
 		'[aria-label="More file actions"]', // `isSingleFile`
 		'[data-hotkey="Mod+s"]', // `isEditingFile`
diff --git a/source/features/mark-merge-commits-in-list.tsx b/source/features/mark-merge-commits-in-list.tsx
index b8435332..859a94ac 100644
--- a/source/features/mark-merge-commits-in-list.tsx
+++ b/source/features/mark-merge-commits-in-list.tsx
@@ -11,6 +11,7 @@ import features from '../feature-manager.js';
 import api from '../github-helpers/api.js';
 import {commitHashLinkInLists, commitTitleInLists} from '../github-helpers/selectors.js';
 import {assertCommitHash} from '../github-helpers/index.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const filterMergeCommits = async (commits: string[]): Promise<string[]> => {
 	const {repository} = await api.v4(`
@@ -64,6 +65,7 @@ async function markCommits(commits: HTMLElement[]): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe([
 		'[data-testid="commit-row-item"]',
 
diff --git a/source/features/pinned-issues-update-time.tsx b/source/features/pinned-issues-update-time.tsx
index 109add41..323c35ab 100644
--- a/source/features/pinned-issues-update-time.tsx
+++ b/source/features/pinned-issues-update-time.tsx
@@ -9,6 +9,7 @@ import api from '../github-helpers/api.js';
 import {getRepo} from '../github-helpers/index.js';
 import looseParseInt from '../helpers/loose-parse-int.js';
 import observe from '../helpers/selector-observer.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type IssueInfo = {
 	updatedAt: string;
@@ -54,6 +55,7 @@ async function update(pinnedIssues: HTMLElement[]): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('.pinned-issue-item', batchedFunction(update, {delay: 100}), {signal});
 }
 
diff --git a/source/features/pr-filters.tsx b/source/features/pr-filters.tsx
index fdbdb188..b55242ff 100644
--- a/source/features/pr-filters.tsx
+++ b/source/features/pr-filters.tsx
@@ -9,6 +9,7 @@ import api from '../github-helpers/api.js';
 import observe from '../helpers/selector-observer.js';
 import {cacheByRepo} from '../github-helpers/index.js';
 import HasChecks from './pr-filters.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const reviewsFilterSelector = '#reviews-select-menu';
 
@@ -86,6 +87,7 @@ async function addChecksFilter(reviewsFilter: HTMLElement): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe(reviewsFilterSelector, addChecksFilter, {signal});
 	observe(`${reviewsFilterSelector} .SelectMenu-list`, addDraftFilter, {signal});
 }
diff --git a/source/features/previous-version.tsx b/source/features/previous-version.tsx
index b46026d2..1e0d485a 100644
--- a/source/features/previous-version.tsx
+++ b/source/features/previous-version.tsx
@@ -9,6 +9,7 @@ import api from '../github-helpers/api.js';
 import GitHubFileURL from '../github-helpers/github-file-url.js';
 import previousVersionQuery from './previous-version.gql';
 import onReactPageUpdate from '../github-events/on-react-page-update.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function getPreviousCommitForFile(pathname: string): Promise<string | undefined> {
 	const {user, repository, branch, filePath} = new GitHubFileURL(pathname);
@@ -89,6 +90,7 @@ async function add(historyButton: HTMLAnchorElement, {signal}: SignalAsOptions):
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('a:has([data-component="leadingVisual"] svg.octicon-history)', add, {signal});
 }
 
diff --git a/source/features/profile-gists-link.tsx b/source/features/profile-gists-link.tsx
index d742e76c..9bee194f 100644
--- a/source/features/profile-gists-link.tsx
+++ b/source/features/profile-gists-link.tsx
@@ -11,6 +11,7 @@ import createDropdownItem from '../github-helpers/create-dropdown-item.js';
 import observe from '../helpers/selector-observer.js';
 import GetGistCount from './profile-gists-link.gql';
 import {repoUnderlineNavDropdownUl} from '../github-helpers/selectors.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const gistCount = new CachedFunction('gist-count', {
 	async updater(username: string): Promise<number> {
@@ -70,6 +71,7 @@ async function appendTab(navigationBar: Element): Promise<void> {
 }
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('nav[aria-label="User"] > ul', appendTab, {signal});
 }
 
diff --git a/source/features/release-download-count.tsx b/source/features/release-download-count.tsx
index eb490713..fc89edac 100644
--- a/source/features/release-download-count.tsx
+++ b/source/features/release-download-count.tsx
@@ -16,6 +16,7 @@ import features from '../feature-manager.js';
 import api from '../github-helpers/api.js';
 import observe from '../helpers/selector-observer.js';
 import {createHeatIndexFunction} from '../helpers/math.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type Asset = {
 	name: string;
@@ -73,7 +74,9 @@ async function addCounts(assetsList: HTMLElement): Promise<void> {
 	}
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	observe('.Box-footer .Box--condensed:has(.octicon-package)', addCounts, {signal});
 }
 
diff --git a/source/features/releases-dropdown.tsx b/source/features/releases-dropdown.tsx
index 7e2149b2..7297ed29 100644
--- a/source/features/releases-dropdown.tsx
+++ b/source/features/releases-dropdown.tsx
@@ -8,6 +8,7 @@ import features from '../feature-manager.js';
 import {buildRepoURL, cacheByRepo} from '../github-helpers/index.js';
 import observe from '../helpers/selector-observer.js';
 import GetReleases from './releases-dropdown.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 const getReleases = new CachedFunction('releases', {
 	async updater(): Promise<string[]> {
@@ -46,6 +47,7 @@ async function addList(searchField: HTMLInputElement): Promise<void> {
 
 const searchFieldSelector = 'input#release-filter';
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe(searchFieldSelector, addList, {signal});
 	delegate(searchFieldSelector, 'input', selectionHandler, {signal});
 }
diff --git a/source/features/repo-header-info.tsx b/source/features/repo-header-info.tsx
index cf6e12f8..04813349 100644
--- a/source/features/repo-header-info.tsx
+++ b/source/features/repo-header-info.tsx
@@ -13,6 +13,7 @@ import api from '../github-helpers/api.js';
 import GetRepositoryInfo from './repo-header-info.gql';
 import {buildRepoURL, cacheByRepo} from '../github-helpers/index.js';
 import abbreviateNumber from '../helpers/abbreviate-number.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type RepositoryInfo = {
 	isFork: boolean;
@@ -72,7 +73,8 @@ async function add(repoLink: HTMLAnchorElement): Promise<void> {
 	}
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('.AppHeader-context-full li:last-child a.AppHeader-context-item', add, {signal});
 }
 
diff --git a/source/features/repo-wide-file-finder.tsx b/source/features/repo-wide-file-finder.tsx
index 407be229..d292e416 100644
--- a/source/features/repo-wide-file-finder.tsx
+++ b/source/features/repo-wide-file-finder.tsx
@@ -6,8 +6,10 @@ import getDefaultBranch from '../github-helpers/get-default-branch.js';
 import {buildRepoURL} from '../github-helpers/index.js';
 import getCurrentGitRef from '../github-helpers/get-current-git-ref.js';
 import {registerHotkey} from '../github-helpers/hotkey.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	const ref = getCurrentGitRef() ?? await getDefaultBranch();
 	const url = buildRepoURL('tree', ref) + '?search=1';
 	registerHotkey('t', url, {signal});
diff --git a/source/features/restore-file.tsx b/source/features/restore-file.tsx
index 07d5aff4..ace73806 100644
--- a/source/features/restore-file.tsx
+++ b/source/features/restore-file.tsx
@@ -9,6 +9,7 @@ import showToast from '../github-helpers/toast.js';
 import {getBranches} from '../github-helpers/pr-branches.js';
 import getPrInfo from '../github-helpers/get-pr-info.js';
 import observe from '../helpers/selector-observer.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function getMergeBaseReference(): Promise<string> {
 	const {base, head} = getBranches();
@@ -116,7 +117,9 @@ function add(editFile: HTMLAnchorElement): void {
 	);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
+
 	observe('.js-file-header-dropdown a[aria-label^="Change this"]', add, {signal});
 
 	// `capture: true` required to be fired before GitHub's handlers
diff --git a/source/features/show-associated-branch-prs-on-fork.tsx b/source/features/show-associated-branch-prs-on-fork.tsx
index b7fdde20..f77d02c9 100644
--- a/source/features/show-associated-branch-prs-on-fork.tsx
+++ b/source/features/show-associated-branch-prs-on-fork.tsx
@@ -14,6 +14,7 @@ import features from '../feature-manager.js';
 import api from '../github-helpers/api.js';
 import {cacheByRepo} from '../github-helpers/index.js';
 import AssociatedPullRequests from './show-associated-branch-prs-on-fork.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type PullRequest = {
 	timelineItems: {
@@ -89,7 +90,8 @@ async function addLink(branch: HTMLElement): Promise<void> {
 	);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	// Memoize because it's being called twice for each. Ideally this should be part of the selector observer
 	// https://github.com/refined-github/refined-github/pull/7194#issuecomment-1894972091
 	observe('react-app[app-name=repos-branches] a[class^=BranchName-] div[title]', memoize(addLink), {signal});
diff --git a/source/features/show-names.tsx b/source/features/show-names.tsx
index ea976876..52b26347 100644
--- a/source/features/show-names.tsx
+++ b/source/features/show-names.tsx
@@ -9,6 +9,7 @@ import {getUsername, isUsernameAlreadyFullName} from '../github-helpers/index.js
 import observe from '../helpers/selector-observer.js';
 import {removeTextNodeContaining} from '../helpers/dom-utils.js';
 import {usernameLinksSelector} from '../github-helpers/selectors.js';
+import {expectToken} from '../github-helpers/github-token.js';
 
 async function dropExtraCopy(link: HTMLAnchorElement): Promise<void> {
 	// Drop 'commented' label to shorten the copy
@@ -67,7 +68,7 @@ async function updateLinks(found: HTMLAnchorElement[]): Promise<void> {
 	}
 }
 
-const updateLink = batchedFunction(updateLinks, {delay: 100});
+const updateLink = batchedFunction(updateLinks, {delay: 200});
 
 function updateDom(link: HTMLAnchorElement): void {
 	// `dropExtraCopy` is async so that errors in this part don't break the entire feature
@@ -76,7 +77,8 @@ function updateDom(link: HTMLAnchorElement): void {
 	updateLink(link);
 }
 
-function init(signal: AbortSignal): void {
+async function init(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	document.body.classList.add('rgh-show-names');
 	observe(usernameLinksSelector, updateDom, {signal});
 }
diff --git a/source/features/tags-on-commits-list.tsx b/source/features/tags-on-commits-list.tsx
index 0a608707..7f12ce9f 100644
--- a/source/features/tags-on-commits-list.tsx
+++ b/source/features/tags-on-commits-list.tsx
@@ -9,6 +9,7 @@ import api from '../github-helpers/api.js';
 import {getCommitHash} from './mark-merge-commits-in-list.js';
 import {buildRepoURL, getRepo} from '../github-helpers/index.js';
 import GetTagsOnCommit from './tags-on-commits-list.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type CommitTags = Record<string, string[]>;
 
@@ -84,6 +85,7 @@ async function getTags(lastCommit: string, after?: string): Promise<CommitTags>
 }
 
 async function init(): Promise<void | false> {
+	await expectToken();
 	const cacheKey = `tags:${getRepo()!.nameWithOwner}`;
 
 	const commitsOnPage = $$('[data-testid="commit-row-item"]');
diff --git a/source/features/useful-not-found-page.tsx b/source/features/useful-not-found-page.tsx
index 1505cbfc..f1ae6f33 100644
--- a/source/features/useful-not-found-page.tsx
+++ b/source/features/useful-not-found-page.tsx
@@ -11,6 +11,7 @@ import getDefaultBranch from '../github-helpers/get-default-branch.js';
 import {getCleanPathname, isUrlReachable} from '../github-helpers/index.js';
 import observe from '../helpers/selector-observer.js';
 import GetLatestCommitToFile from './useful-not-found-page.gql';
+import {expectToken} from '../github-helpers/github-token.js';
 
 type File = {
 	previous_filename?: string;
@@ -184,7 +185,9 @@ async function showGitObjectHistoryOnRepo(description: HTMLDivElement): Promise<
 	}
 }
 
-function init(): void {
+async function init(): Promise<void> {
+	await expectToken();
+
 	void showDefaultBranchLink();
 	void showGitObjectHistory();
 }
@@ -201,7 +204,8 @@ async function initPRCommit(): Promise<void | false> {
 	);
 }
 
-function initRepoFile(signal: AbortSignal): void {
+async function initRepoFile(signal: AbortSignal): Promise<void> {
+	await expectToken();
 	observe('#repos-header-breadcrumb-wide-heading + ol a', crossIfNonExistent, {signal});
 	observe('main div[data-testid="eror-404-description"]', showGitObjectHistoryOnRepo, {signal});	// "eror" as misspelled by GitHub
 }
diff --git a/source/features/user-local-time.tsx b/source/features/user-local-time.tsx
index 661a5985..7ca62ec0 100644
--- a/source/features/user-local-time.tsx
+++ b/source/features/user-local-time.tsx
@@ -156,10 +156,7 @@ async function insertUserLocalTime(hovercardContainer: Element): Promise<void> {
 	void display({datePromise, placeholder, container});
 }
 
-const selector = [
-	'.js-hovercard-content .Popover-message div.d-flex.mt-3.overflow-hidden > div.d-flex',
-	'.js-hovercard-content .Popover-message div.d-flex.mt-3 > div.overflow-hidden.ml-3', // GHE 2022/06/24
-];
+const selector = '.js-hovercard-content .Popover-message div.d-flex.mt-3.overflow-hidden > div.d-flex';
 
 function init(signal: AbortSignal): void {
 	observe(selector, insertUserLocalTime, {signal});
diff --git a/source/github-helpers/api.ts b/source/github-helpers/api.ts
index b0dba25b..498cc8a8 100644
--- a/source/github-helpers/api.ts
+++ b/source/github-helpers/api.ts
@@ -166,6 +166,10 @@ const v4uncached = async (
 ): Promise<AnyObject> => {
 	const personalToken = await getToken();
 
+	if (!personalToken) {
+		throw new RefinedGitHubAPIError('Personal token required for this feature');
+	}
+
 	// TODO: Remove automatic usage of globals via `getRepo()`
 	// https://github.com/refined-github/refined-github/issues/5821
 	const currentRepoIfAny = getRepo(); // Don't destructure, it's `undefined` outside repos
diff --git a/source/github-helpers/index.ts b/source/github-helpers/index.ts
index 429564a5..4feafcef 100644
--- a/source/github-helpers/index.ts
+++ b/source/github-helpers/index.ts
@@ -51,7 +51,7 @@ export function isUsernameAlreadyFullName(username: string, realname: string): b
 		.replaceAll(/\W/g, '')
 		.toLowerCase();
 
-	return username === realname;
+	return username === realname || username.startsWith(realname);
 }
 
 const validVersion = /^[vr]?\d+(?:\.\d+)+/;


Test patch:
diff --git a/build/features.test.ts b/build/features.test.ts
index 2e6de30f..6cbed1b9 100755
--- a/build/features.test.ts
+++ b/build/features.test.ts
@@ -149,6 +149,12 @@ function validateTsx(file: FeatureFile): void {
 
 	assert(/test url/i.test(file.contents().toString()), 'Should have test URLs');
 
+	if (/api\.v4|getDefaultBranch/.test(String(file.contents())) && /observe\(|delegate\(/.test(String(file.contents()))) {
+		assert(
+			file.contents().includes('await expectToken()'),
+			`${file.id} uses the v4 API, so it should include await expectToken() in its init function`,
+		);
+	}
 	if (file.contents().includes('.addCssFeature')) {
 		assert(
 			!file.contents().includes('.add('),
diff --git a/source/github-helpers/index.test.ts b/source/github-helpers/index.test.ts
index e3a7e40c..fd82c04f 100644
--- a/source/github-helpers/index.test.ts
+++ b/source/github-helpers/index.test.ts
@@ -91,6 +91,7 @@ test('isUsernameAlreadyFullName', () => {
 	assert.isTrue(isUsernameAlreadyFullName('john-wdoe', 'John W. Doe'));
 	assert.isTrue(isUsernameAlreadyFullName('john-doe-jr', 'John Doe Jr.'));
 	assert.isTrue(isUsernameAlreadyFullName('nicolo', 'Nicolò'));
+	assert.isTrue(isUsernameAlreadyFullName('johnwashere', 'John'));
 	assert.isFalse(isUsernameAlreadyFullName('dotconnor', 'Connor Love'));
 	assert.isFalse(isUsernameAlreadyFullName('fregante ', 'Federico Brigante'));
 });