add friend sync suggestions배포 정보 없음 — 사용자·Apex 실험의 전체 배포 비율을 알려 주는 공개 출처가 없습니다. 본인 배정은 아래에서 확인할 수 있어요.
아래 콘솔 명령이 복사한 결과를 붙여 넣거나 Discord가 받은 실험 배정 응답(JSON) 파일을 고르면 이 브라우저 안에서만 읽어 이 실험의 배정값을 보여 줍니다. 붙여 넣은 내용과 파일은 전송하거나 저장하지 않습니다. 결과는 Discord가 그 배정을 받은 시점 기준이며 전체 배포 비율은 아닙니다.
지원 형식: 아래 콘솔 명령이 복사한 결과, /api/v9/experiments 응답(assignments), 한 사용자의 /api/v9/apex/experiments 응답. HAR, 토큰이 든 파일, 계정 전체 내보내기는 고르지 마세요.
// 디스코드 아카이브 배정 확인: Discord가 이미 받은 실험 배정만 읽어 클립보드에 복사합니다. 로그인 토큰은 읽지 않고 어디로도 보내지 않습니다.
(() => {
const target = {"apex":false,"hash":3487269433,"unit":null,"text":{"noStore":"Discord 배정 저장소를 찾지 못했습니다. Discord를 새로고침해 로그인이 끝난 뒤 다시 실행하세요.","found":"이 실험 배정:","missing":"이 계정에는 이 실험 배정이 없습니다.","notEligible":"대상 아님","unassigned":"배정된 variant 없음","noApex":"읽을 수 있는 Apex 배정이 없습니다.","unitCounts":"user 단위 {user}개, installation 단위 {installation}개","multipleUnits":"여러 계정·기기의 Apex 배정이 섞인 종류는 결과에서 뺐습니다.","empty":"아직 Discord가 받은 배정이 없습니다. Discord가 다 열린 뒤 다시 실행하세요.","skipped":"형식이 맞지 않아 뺀 배정이 {count}개 있습니다.","copied":"배정 {count}개를 클립보드에 복사했습니다. 디스코드 아카이브의 \"결과 붙여넣기\" 칸에 붙여 넣으세요.","notCopied":"클립보드 복사를 쓸 수 없습니다. 아래 결과를 직접 선택해 복사하세요."}};
const requires = [];
webpackChunkdiscord_app.push([[Symbol()], {}, (require) => { requires.push(require); }]);
webpackChunkdiscord_app.pop();
// 스토어 이름(displayName)이 정확히 같은 것만 모은다. 아무 속성이나 함수로 돌려주는 객체를 저장소로 오인하지 않는다.
const found = { ExperimentStore: [], ApexExperimentStore: [], AuthenticationStore: [] };
for (const require of requires) {
for (const module of Object.values(require.c || {})) {
try {
for (const value of Object.values(module.exports || {})) {
const name = value && value.constructor && value.constructor.displayName;
if (typeof name === "string" && Object.prototype.hasOwnProperty.call(found, name) && !found[name].includes(value)) found[name].push(value);
}
} catch {}
}
}
const size = (object) => (object && typeof object === "object" ? Object.keys(object).length : 0);
const measure = (read) => { try { return read(); } catch { return -1; } };
// 같은 이름이 여럿이면 자료가 가장 많은 것을 쓴다.
const pick = (list, amount) => list
.map((store) => [store, measure(() => amount(store))])
.filter(([, value]) => value >= 0)
.sort((left, right) => right[1] - left[1])
.map(([store]) => store)[0] || null;
const apexRows = (store) => Object.values((store.getState() || {}).evaluatedExperiments || {})
.reduce((sum, units) => sum + Object.values(units || {}).reduce((total, unit) => total + size(unit && unit.assignments), 0), 0);
const userStore = pick(found.ExperimentStore.filter((store) => typeof store.getAllUserExperimentDescriptors === "function"), (store) => size(store.getAllUserExperimentDescriptors()));
const apexStore = pick(found.ApexExperimentStore.filter((store) => typeof store.getState === "function"), apexRows);
const authStore = found.AuthenticationStore.find((store) => typeof store.getId === "function") || null;
const env = window.GLOBAL_ENV || {};
const channel = ["stable", "ptb", "canary"].includes(env.RELEASE_CHANNEL) ? env.RELEASE_CHANNEL : undefined;
// 데스크톱 앱은 DiscordNative를 둔다. 있는지만 보고 호출하지 않는다.
const platform = typeof window.DiscordNative === "undefined" ? "web" : "desktop";
const result = { platform, release_channel: channel, assignments: null };
let message;
let count = 0;
let skipped = 0;
if (!target.apex) {
if (!userStore) return target.text.noStore;
const all = userStore.getAllUserExperimentDescriptors() || {};
const entries = Object.entries(all);
// 이 사이트의 파서가 받는 모양만 넣고, 뺀 수는 결과에 알린다.
result.assignments = entries
.filter(([hash, item]) => item && Number.isInteger(Number(hash)) && Number.isInteger(item.revision) && Number.isInteger(item.bucket))
.map(([hash, item]) => [Number(hash), item.revision, item.bucket, item.override ? 0 : null, null, null, item.aaMode ? 1 : 0]);
count = result.assignments.length;
skipped = entries.length - count;
const mine = all[String(target.hash)];
const label = !mine ? "" : mine.bucket === -1 ? target.text.notEligible : mine.bucket === 0 ? "Control" : "Treatment " + mine.bucket;
message = mine
? target.text.found + " " + label + " · revision " + mine.revision + (mine.override ? " · override" : "") + (mine.aaMode ? " · A/A" : "")
: target.text.missing;
} else {
if (!apexStore) return target.text.noStore;
const evaluated = (apexStore.getState() || {}).evaluatedExperiments || {};
// Discord처럼 user 단위는 현재 계정 ID로 고른다. 보존된 이전 세션 단위가 남을 수 있기 때문이다. ID는 결과에 넣지 않는다.
let currentId = null;
try { currentId = authStore ? authStore.getId() : null; } catch {}
const surfaces = {};
const unitCounts = {};
let mine = null;
let mixed = false;
for (const [unitType, surface] of [["user", "1"], ["installation", "2"]]) {
const units = evaluated[unitType] || {};
const keys = Object.keys(units);
unitCounts[unitType] = keys.length;
let unit = null;
if (unitType === "user" && currentId != null) unit = units[String(currentId)] || null;
else if (keys.length === 1) unit = units[keys[0]];
else if (keys.length > 1) mixed = true;
if (!unit) continue;
const rows = unit.assignments || {};
const entries = Object.entries(rows);
const list = entries
.filter(([hash, item]) => item && Number.isInteger(Number(hash)))
.map(([hash, item]) => [Number(hash), item.variantId ?? null, item.isOverride ? 1 : 0, item.revision ?? null]);
skipped += entries.length - list.length;
// 계정·기기 ID 대신 고정 이름을 쓴다.
surfaces[surface] = { self: { assignments: list } };
count += list.length;
const hit = rows[String(target.hash)];
if (hit && (target.unit === null || target.unit === unitType)) mine = hit;
}
if (Object.keys(surfaces).length === 0) {
const counts = target.text.unitCounts.replace("{user}", String(unitCounts.user)).replace("{installation}", String(unitCounts.installation));
return target.text.noApex + " (" + counts + ")" + (mixed ? " " + target.text.multipleUnits : "");
}
result.assignments = surfaces;
message = mine
? target.text.found + " " + (mine.variantId == null ? target.text.unassigned : "Variant " + mine.variantId) + " · revision " + (mine.revision ?? "?") + (mine.isOverride ? " · override" : "")
: target.text.missing;
if (mixed) message += " " + target.text.multipleUnits;
}
const skippedNote = skipped > 0 ? " " + target.text.skipped.replace("{count}", String(skipped)) : "";
// 아직 배정을 받지 않은 탭이면 빈 결과를 복사하지 않는다.
if (count === 0) return target.text.empty + skippedNote;
const text = JSON.stringify(result);
if (typeof copy === "function") {
copy(text);
return message + "\n" + target.text.copied.replace("{count}", String(count)) + skippedNote;
}
return message + "\n" + target.text.notCopied + skippedNote + "\n" + text;
})()이 명령은 Discord가 이미 받아 둔 실험 배정만 읽어 클립보드에 복사합니다. 토큰·메시지는 읽지 않고 어디로도 보내지 않습니다. 현재 계정 ID는 Apex 배정 중 이 계정 것을 고르는 데만 쓰고, 계정·기기 ID는 결과에 넣지 않습니다. 다른 곳에서 받은 명령이나 토큰을 요구하는 명령은 붙여 넣지 마세요. 붙여 넣기 전에 AI 챗봇에 "이 코드가 토큰을 읽거나 외부로 보내는지" 물어보는 것도 쉬운 확인 방법입니다.
로그인한 계정은 Discord가 배정을 실시간 연결(게이트웨이)로 받기 때문에 네트워크 탭에서 experiments로 걸러도 요청이 보이지 않습니다. 로그아웃 상태에서 보이는 /api/v9/experiments·/api/v9/apex/experiments 응답은 계정이 아니라 이 브라우저 기준 배정이며, 응답 본문을 .json으로 저장해 고를 수 있습니다.
요청 헤더, Copy as cURL·Copy as fetch, HAR 저장(Save all as HAR)은 쓰지 마세요. 로그인한 상태라면 계정 토큰(authorization)이 들어 있어 다른 사람 손에 들어가면 계정을 빼앗길 수 있습니다.
결과 넣기
참여는 선택입니다. 위에서 읽은 파일로 제출할 정제 내용을 먼저 만들고, 그 내용을 확인해 동의해야 제출됩니다. 파일을 고르지 않았거나 동의하지 않으면 아무것도 보내지 않습니다.
먼저 위에서 배정 결과를 읽으면 참여할 수 있습니다.
이 브라우저에서 참여했다면 바로 철회할 수 있습니다. 쿠키를 지웠다면 제출 때 받은 삭제키를 입력하세요.
통계 참여에 동의한 브라우저가 제출한 배정을 날짜별 건수로 합산했습니다. Discord 전체 사용자 대상 배포 비율이나 사용자 수가 아니며, 파일의 진위와 실제 배정 시각은 검증하지 않습니다. 제출 시각은 배정을 관측한 시각이 아니고, 철회해도 이미 합산된 건수는 바뀌지 않습니다.
집계 범위: 2026-10-12 ~ 2026-10-18 (UTC) 제출분 · 하루 단위 갱신
아직 이 실험에서 공개할 만큼의 참여 표본이 없습니다.