Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
c7f031d
🔃 Daily sync: main -> l10n (June 29, 2026)
Jun 29, 2026
0940b03
Bump profiler-cli version to 0.4.0 (#6132)
canova Jun 29, 2026
341ef3c
🔃 Daily sync: main -> l10n (June 30, 2026)
Jun 30, 2026
03b9fc2
Update all Yarn dependencies (2026-07-01) (#6135)
depfu[bot] Jul 1, 2026
e176afe
Add "/" and "f" keyboard shortcuts to focus the panel filter box (#6025)
fqueze Jul 1, 2026
889465e
🔃 Daily sync: main -> l10n (July 2, 2026)
Jul 2, 2026
e160527
Pontoon/Firefox Profiler: Update 21 localizations
mozilla-pontoon Jul 2, 2026
df727b7
Pontoon/Firefox Profiler: Update French (fr), Chinese (Taiwan) (zh-TW…
mozilla-pontoon Jul 2, 2026
f7d8838
Pontoon/Firefox Profiler: Update Greek (el)
mozilla-pontoon Jul 2, 2026
4d4fd0d
Pontoon/Firefox Profiler: Update Frisian (fy-NL)
mozilla-pontoon Jul 2, 2026
9f70d7a
Pontoon/Firefox Profiler: Update German (de)
mozilla-pontoon Jul 2, 2026
8b12d07
Pontoon/Firefox Profiler: Update English (Great Britain) (en-GB)
mozilla-pontoon Jul 2, 2026
2930d23
Pontoon/Firefox Profiler: Update Russian (ru)
mozilla-pontoon Jul 2, 2026
7bf57d6
Change the derived StackTable's prefix column to Int32Array.
mstange May 16, 2026
817da4a
Implement column sorting in the generic TreeView.
mstange Mar 31, 2026
45740c3
Implement column sorting in the marker table.
mstange Mar 31, 2026
7f0adba
Pontoon/Firefox Profiler: Update Interlingua (ia)
mozilla-pontoon Jul 2, 2026
2880a96
Change the raw stack table to store the prefix as an index offset.
mstange Jul 2, 2026
e3608a4
Change profile.shared.stackTable.prefix to prefixOffset (delta from c…
mstange Jul 3, 2026
aead9de
Add workflow to insert PR preview links (#6133)
skylarkning Jul 3, 2026
cc7309c
Merge branch 'main' into push-nuyuywpytuqu
mstange Jul 3, 2026
63db825
Implement column sorting in the marker table (#6083)
mstange Jul 3, 2026
76dc0ca
Use callNodeInfo.fieldForNode(i) instead of callNodeTable.field[i] in…
mstange Jun 21, 2026
c765a85
Add a startsAtBottom prop to FlameGraph.
mstange Jun 21, 2026
1875baf
Make it easier to create upside-down flame graphs (#6128)
mstange Jul 3, 2026
17c959f
🔃 Daily sync: main -> l10n (July 4, 2026)
Jul 4, 2026
b747581
Use appropriate highlighter dialects for ts + jsx.
mstange Jul 5, 2026
7da93e4
⬆️ Update @tgwf/co2 to version 0.19.0 (#6143)
depfu[bot] Jul 6, 2026
4e4a5d6
🔃 Daily sync: main -> l10n (July 6, 2026)
Jul 6, 2026
3b200e3
Merge branch 'main' into push-ytyswvrmuuyr
mstange Jul 6, 2026
f77119c
Use appropriate highlighter dialects for ts + jsx (#6141)
mstange Jul 6, 2026
5fe3c07
style: add .editorconfig (#6142)
cathaysia Jul 6, 2026
954c3a2
Prevent long source URLs from overflowing the code error overlay (#6147)
canova Jul 6, 2026
ad52de3
Extract a runCommand helper for profiler-cli command handlers (#6148)
canova Jul 6, 2026
0085b2a
Pontoon/Firefox Profiler: Update Spanish (Chile) (es-CL)
mozilla-pontoon Jul 6, 2026
a812a34
feat(theme): add color-scheme meta
cathaysia Jul 7, 2026
c150333
feat(theme): add color-scheme meta (#6150)
mstange Jul 7, 2026
422bae0
feat: invert call stack in flame graph to render as icicle graph
cathaysia Jun 5, 2026
5d397d9
Refactor: Split invertCallstack state into invertCallTree and invertF…
cathaysia Jul 7, 2026
25b7b9f
fix(flame-graph): show traced timing in icicle tooltips
cathaysia Jul 7, 2026
a542753
fix(stack-chart): ignore call tree inversion state
cathaysia Jul 7, 2026
4fa6dc9
🔃 Daily sync: main -> l10n (July 7, 2026)
Jul 7, 2026
aeb4192
Add an early throw for the this.querier and remove the non-null asser…
canova Jul 7, 2026
b894055
Merge branch 'main' into feat/icicle_graph
mstange Jul 7, 2026
4302ef2
feat: invert call stack in flame graph to render as icicle graph (#6090)
mstange Jul 7, 2026
0f717c1
Improve preview link path discovery
skylarkning Jul 6, 2026
b2c0eba
Merge branch 'main' into improve-preview-link-discovery
mstange Jul 7, 2026
faf67a7
Improve preview link discovery (#6149)
mstange Jul 7, 2026
b6e4176
🔃 Daily sync: main -> l10n (July 8, 2026)
Jul 8, 2026
630fb0c
Update all Yarn dependencies (2026-07-08) (#6153)
depfu[bot] Jul 8, 2026
be35857
🔃 Daily sync: main -> l10n (July 9, 2026)
Jul 9, 2026
3823427
Pontoon/Firefox Profiler: Update Turkish (tr)
mozilla-pontoon Jul 9, 2026
b1ffe85
🔃 Sync: l10n -> main (July 9, 2026) (#6155)
canova Jul 9, 2026
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
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{json,toml,yml,yaml,css}]
indent_style = space
indent_size = 2

[*.{js,ts,jsx,tsx,mjs,cjs}]
indent_style = space
indent_size = 2
286 changes: 286 additions & 0 deletions .github/scripts/add-preview-links.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import fs from 'fs/promises';
import { pathToFileURL } from 'url';

export const START_MARKER = '<!-- profiler-preview-links:start -->';
export const END_MARKER = '<!-- profiler-preview-links:end -->';

const GITHUB_API_URL = 'https://api.github.com';
const GITHUB_API_VERSION = '2026-03-10';
const NETLIFY_HOST_SUFFIX = '--perf-html.netlify.app';
const MAIN_BASE_URL = `https://main${NETLIFY_HOST_SUFFIX}`;
const PROFILE_HOST = 'profiler.firefox.com';
const SHARE_HOST = 'share.firefox.dev';

function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

const PROFILE_URL_HOSTS_REGEXP = [
'share\\.firefox\\.dev',
'profiler\\.firefox\\.com',
`[a-z0-9-]+${escapeRegExp(NETLIFY_HOST_SUFFIX)}`,
].join('|');

export function hasDeployPreviewLink(body) {
return (
body.includes(START_MARKER) ||
/https:\/\/deploy-preview-\d+--perf-html\.netlify\.app(?:\/|\b)/.test(body)
);
}

export function extractIssueNumbers(
text,
owner = 'firefox-devtools',
repo = 'profiler'
) {
const issueNumbers = new Set();
const markdownIssueRegExp = /(?:^|[^\w/-])#(\d+)\b/g;
const issueUrlRegExp = new RegExp(
`https://github\\.com/${escapeRegExp(owner)}/${escapeRegExp(
repo
)}/(?:issues|pull)/(\\d+)\\b`,
'g'
);

for (const match of text.matchAll(markdownIssueRegExp)) {
issueNumbers.add(Number(match[1]));
}

for (const match of text.matchAll(issueUrlRegExp)) {
issueNumbers.add(Number(match[1]));
}

return [...issueNumbers];
}

export function extractProfileUrls(text) {
const urls = [];
const profileUrlRegExp = new RegExp(
`https://(?:${PROFILE_URL_HOSTS_REGEXP})/[^\\s<>)\\]]+`,
'g'
);

for (const match of text.matchAll(profileUrlRegExp)) {
// Profile links are often followed by punctuation in prose, for example
// "Profile: https://share.firefox.dev/466MJwC.".
urls.push(match[0].replace(/[.,;:]+$/, ''));
}

return urls;
}

export function profileUrlToPath(profileUrl) {
const url = new URL(profileUrl);

if (
url.hostname !== PROFILE_HOST &&
!url.hostname.endsWith(NETLIFY_HOST_SUFFIX)
) {
return null;
}

const path = `${url.pathname}${url.search}${url.hash}`;
return path === '/' ? null : path;
}

// Returns the profile path from profiler.firefox.com, main, or deploy preview
// URLs, and follows share.firefox.dev redirects to resolve short links.
export async function resolveProfileUrlToPath(profileUrl, fetchImpl = fetch) {
const url = new URL(profileUrl);
const path = profileUrlToPath(profileUrl);

if (path) {
return path;
}

if (url.hostname !== SHARE_HOST) {
return null;
}

try {
const response = await fetchImpl(profileUrl, { redirect: 'follow' });
return profileUrlToPath(response.url);
} catch (error) {
console.warn(`Could not resolve ${profileUrl}: ${error.message}`);
return null;
}
}

export function normalizePath(path) {
if (!path || path === '/') {
return '/';
}

return path.startsWith('/') ? path : `/${path}`;
}

export function buildPreviewLinks(prNumber, path) {
const normalizedPath = normalizePath(path);
const previewBaseUrl = `https://deploy-preview-${prNumber}--perf-html.netlify.app`;

return `[Main](${MAIN_BASE_URL}${normalizedPath}) | [Deploy preview](${previewBaseUrl}${normalizedPath})`;
}

export function addPreviewLinksToBody(body, previewLinks) {
const previewLinksBlock = `${START_MARKER}\n${previewLinks}\n${END_MARKER}`;
const trimmedBody = body.trim();

if (!trimmedBody) {
return previewLinksBlock;
}

return `${previewLinksBlock}\n\n${trimmedBody}`;
}

async function githubRequest(path, token, options = {}) {
const response = await fetch(`${GITHUB_API_URL}${path}`, {
...options,
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'content-type': 'application/json',
'user-agent': 'firefox-profiler-preview-links',
'x-github-api-version': GITHUB_API_VERSION,
...options.headers,
},
});

if (!response.ok) {
const message = await response.text();
throw new Error(
`GitHub API request failed: ${response.status} ${response.statusText}\n${message}`
);
}

if (response.status === 204) {
return null;
}

return response.json();
}

async function getIssueTexts({ owner, repo, issueNumber, token }) {
const issue = await githubRequest(
`/repos/${owner}/${repo}/issues/${issueNumber}`,
token
);
const comments = await githubRequest(
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`,
token
);

return [issue.body ?? '', ...comments.map((comment) => comment.body ?? '')];
}

async function getPullRequest({ owner, repo, pullNumber, token }) {
return githubRequest(`/repos/${owner}/${repo}/pulls/${pullNumber}`, token);
}

// Finds the first profile path mentioned in the PR or its linked issues.
// Returns '/' if no profile link is found, so generated links use the homepage.
async function findProfilePath({ owner, repo, pullRequest, token }) {
const pullRequestText = `${pullRequest.title ?? ''}\n${pullRequest.body ?? ''}`;

for (const profileUrl of extractProfileUrls(pullRequestText)) {
const path = await resolveProfileUrlToPath(profileUrl);

if (path) {
return path;
}
}

for (const issueNumber of extractIssueNumbers(pullRequestText, owner, repo)) {
let issueTexts;

try {
issueTexts = await getIssueTexts({ owner, repo, issueNumber, token });
} catch (error) {
console.warn(`Could not read issue #${issueNumber}: ${error.message}`);
continue;
}

for (const issueText of issueTexts) {
for (const profileUrl of extractProfileUrls(issueText)) {
const path = await resolveProfileUrlToPath(profileUrl);

if (path) {
return path;
}
}
}
}

return '/';
}

export async function main() {
const eventPath = process.env.GITHUB_EVENT_PATH;
const repository = process.env.GITHUB_REPOSITORY;
const token = process.env.GITHUB_TOKEN;

if (!eventPath || !repository || !token) {
throw new Error(
'GITHUB_EVENT_PATH, GITHUB_REPOSITORY, and GITHUB_TOKEN are required.'
);
}

const payload = JSON.parse(await fs.readFile(eventPath, 'utf8'));
const pullRequest = payload.pull_request;

if (!pullRequest) {
console.log('This event does not include a pull request. Nothing to do.');
return;
}

const [owner, repo] = repository.split('/');
const body = pullRequest.body ?? '';

if (hasDeployPreviewLink(body)) {
console.log('The pull request already has deploy preview links.');
return;
}

const profilePath = await findProfilePath({
owner,
repo,
pullRequest,
token,
});
const previewLinks = buildPreviewLinks(pullRequest.number, profilePath);

// Re-read the PR body just before patching it. GitHub's API does not support
// conditional PATCH requests here, so this is a best-effort guard against
// overwriting edits that happened while this script looked up the profile URL.
const latestPullRequest = await getPullRequest({
owner,
repo,
pullNumber: pullRequest.number,
token,
});
const latestBody = latestPullRequest.body ?? '';

if (hasDeployPreviewLink(latestBody)) {
console.log('The pull request already has deploy preview links.');
return;
}

const updatedBody = addPreviewLinksToBody(latestBody, previewLinks);

await githubRequest(
`/repos/${owner}/${repo}/pulls/${pullRequest.number}`,
token,
{
body: JSON.stringify({ body: updatedBody }),
method: 'PATCH',
}
);

console.log(`Added preview links to pull request #${pullRequest.number}.`);
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
await main();
}
Loading
Loading