The following is a similar query to the one in Glorfindel's answer, but implemented as a runnable snippet and using the Stack Exchange API (as the load on the API can be quite heavy, please procure the API key on StackApps or use an existing one).
The generated report includes the following stats:
- link to the answering user profile
- total number of answers by the user
- total number of community wiki answers
- total number of accepted answers
- total reputation earned via bounties
- total score of answers
- rough total reputation earned estimate
Example report for a user id 953887 on Stack Overflow:
/// <reference types="@userscripters/stackexchange-api-types" />
const API_BASE = "https://api.stackexchange.com";
const API_VER = 2.3;
/**
* @summary delays script execution
* @param {number} [ms] milliseconds to delay for
* @returns {Promise<void>}
*/
const delay = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* @summary makes a table cell
* @param {string| Node} content
* @returns {HTMLTableCellElement}
*/
const makeCell = (content) => {
const td = document.createElement("td");
td.append(content);
return td;
};
/**
* @summary makes a table row
* @param {Array<string | Node>} columns
* @returns {HTMLTableRowElement}
*/
const makeRow = (...columns) => {
const tr = document.createElement("tr");
const cells = columns.map(makeCell);
tr.append(...cells);
return tr;
};
const makeLink = (url, label) => {
const a = document.createElement("a");
a.target = "_blank";
a.href = url;
a.innerText = label;
return a;
};
/**
* @see https://api.stackexchange.com/docs/questions-on-users
*
* @summary gets question info by user id
* @param {number} id user id
* @param {{ site?: string, page?: number, ...rest }} [options] request configuration
* @returns {Promise<StackExchangeAPI.Question[]>}
*/
const getQuestions = async (id, { site = "stackoverflow", page = 1, ...rest }) => {
const url = new URL(`${API_BASE}/${API_VER}/users/${id}/questions`);
url.search = new URLSearchParams({
site,
page: page.toString(),
...rest,
}).toString();
const res = await fetch(url.toString());
if (!res.ok) return [];
const { items = [], has_more = false, backoff } = await res.json();
if (backoff) {
await delay(backoff * 1e3);
return getQuestions(id, { site, page, ...rest });
}
if (has_more) {
items.push(
...(await getQuestions(id, {
site,
page: page + 1,
...rest,
}))
);
}
return items;
};
/**
* @see https://api.stackexchange.com/docs/answers-on-questions
*
* @summary gets answers by question ids
* @param {number[]} ids user id
* @param {{ site?: string, page?: number, ...rest }} [options] request configuration
* @returns {Promise<StackExchangeAPI.Answer[]>}
*/
const getAnswers = async (ids, { site = "stackoverflow", page = 1, ...rest }) => {
const url = new URL(`${API_BASE}/${API_VER}/questions/${ids.join(";")}/answers`);
url.search = new URLSearchParams({
site,
page: page.toString(),
...rest,
}).toString();
const res = await fetch(url.toString());
if (!res.ok) return [];
const { items = [], has_more = false, backoff } = await res.json();
if (backoff) {
await delay(backoff * 1e3);
return getAnswers(ids, { site, page, ...rest });
}
if (has_more) {
items.push(
...(await getAnswers(ids, {
site,
page: page + 1,
...rest,
}))
);
}
return items;
};
/**
* @template {unknown} T
*
* @summary splits an array into chunks
* @param {T[]} arr array to split
* @param {number} size chunk size
* @returns {T[][]}
*/
const chunkify = (arr, size) => {
/** @type {T[][]} */
const output = [];
arr.forEach((e, i) => {
let chunk = output[output.length - 1];
i % size ? chunk.push(e) : output.push([e]);
});
return output;
};
/**
* @template {object} T
*
* @summary get a sum of items by key
* @param {T[]} items items to process
* @param {keyof T} key key to use for the ssum
* @returns {number}
*/
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);
/**
/**
* @template {object} T
*
* @summary get a total of items by key
* @param {T[]} items items to process
* @param {keyof T} key key to use for the total
* @returns {number}
*/
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);
/**
* @summary sums score of all posts
* @param {Array<StackExchangeAPI.Post>} posts posts to process
* @returns {number}
*/
const getTotalScore = (posts) => getSum(posts, "score");
window.addEventListener("load", () => {
const apiKeyInput = document.getElementById("api_key");
const accountInput = document.getElementById("account_id");
const submit = document.getElementById("search");
const siteInput = document.getElementById("site");
const progress = document.getElementById("status");
progress.value = 0;
submit.addEventListener("click", async () => {
const { value: key } = apiKeyInput;
const { value: site } = siteInput;
const { value: userId } = accountInput;
const report = document.querySelector("#preview > tbody");
const questions = await getQuestions(userId, { key, site });
progress.value = 25;
const questionIds = questions.map(({ question_id }) => question_id);
const chunks = chunkify(questionIds, 100); // max number of ids is 100
const { length: numChunks } = chunks;
const progressValuePerChunk = 75 / numChunks;
/** @type {Record<string, { answers: StackExchangeAPI.Answer[], link: string, name: string }>} */
const answers = {};
for (const chunk of chunks) {
const items = await getAnswers(chunk, { key, site, filter: "!nKzQUREjAc" });
items.forEach((answer) => {
const { owner: { user_id, display_name, link } } = answer;
if (!answers[user_id]) answers[user_id] = { answers: [] };
const userAnswers = answers[user_id];
userAnswers.answers.push(answer);
userAnswers.link = link;
userAnswers.name = display_name;
});
progress.value += progressValuePerChunk;
}
report.querySelectorAll("tr").forEach((r) => r.remove());
const answersByUser = Object.values(answers);
answersByUser.sort((a, b) => getTotalScore(b.answers) - getTotalScore(a.answers));
const upvoteRep = 10;
const acceptRep = 15;
const rows = answersByUser.flatMap(({ link, name, answers }) => {
const userLink = makeLink(link, name);
const totalAnswers = answers.length;
const totalAccepted = getTotal(answers, "is_accepted");
const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
const totalBounties = getSum(answers, "awarded_bounty_amount");
const totalScore = getTotalScore(answers);
const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
const repEstimate = totalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;
return makeRow(
userLink,
totalAnswers,
totalWikis,
totalAccepted,
totalBounties,
totalScore,
repEstimate
);
});
report.append(...rows);
report.closest("table").classList.remove("hidden");
progress.value = 100;
});
});
label {
display: block;
font-weight: 600;
}
caption,
label {
font-weight: 600;
margin-bottom: 0.5vh;
}
table {
margin-top: 4vh;
border-collapse: collapse;
}
th,
td {
padding: 2vh 2vw;
border: 1px solid lightgray;
}
.hidden {
display: none;
}
progress {
margin-top: 2vh;
height: 20px;
border-radius: 0;
}
<form>
<label for="api_key">SE API key</label>
<input id="api_key" type="text" title="SE API key" placeholder="abc12345" />
<label for="site">Site API Slug</label>
<input id="site" type="text" title="Site API slug" placeholder="stackoverflow" value="stackoverflow" />
<label for="account_id">User Account Id</label>
<input id="account_id" type="text" title="Account Id" placeholder="12345" />
<button id="search" type="button">Get Info</button>
</form>
<progress id="status" max="100" value="0"></progress>
<table id="preview" class="hidden" cellspacing="0">
<caption>Stack Exchange Post Activity</caption>
<thead>
<tr>
<th>Account</th>
<th>Answers</th>
<th>Wikis</th>
<th>Accepted</th>
<th>Bounty rep</th>
<th>Score</th>
<th>Rep Estimate</th>
</tr>
</thead>
<tbody>
</tbody>
</table>