From 7e09947e62921fb62a89701b3f72bc5f1c4d8b06 Mon Sep 17 00:00:00 2001 From: CRT Date: Tue, 13 Oct 2026 01:38:50 +0800 Subject: [PATCH] console verify App Privacy: real page text (category-only summary sections, ' | ' joined lines, Apple's commas); tracking label without 'data data'; Play Data safety page path confirmed --- CHANGELOG.md | 6 ++++++ cli/lib/console-plan.ts | 4 +++- cli/lib/console-play-plans.ts | 2 +- cli/lib/console-verify.ts | 2 +- tests/console-plan.test.ts | 27 ++++++++++++++++++++++++++- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6f40b..ec5cddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- `console verify apple-app-privacy` checked against real page text (2026-10-12): the summary sections ("Data Used to + Track You" and the others) list category names only and are skipped; a type's lines may come joined by " | "; Apple's + comma style ("X, and Y") and the straight apostrophe parse. The tracking radio label of a type that already ends in + "Data" no longer reads "crash data data". The Play Data safety page path (`app-content/data-privacy-security`, with + Export / Import CSV) is confirmed. + - Fix (`play build` / `check` versionCode source warning): a variable set from the property first (`def code = (project.findProperty('versionCode') ?: ...).toInteger()`, then `versionCode code`; also `ext.` and Kotlin `val`) is followed to its definition and no longer flagged. diff --git a/cli/lib/console-plan.ts b/cli/lib/console-plan.ts index 48a7ea6..301f270 100644 --- a/cli/lib/console-plan.ts +++ b/cli/lib/console-plan.ts @@ -107,7 +107,9 @@ export function nameText(n: Name): string { /** App Privacy labels verified on the real dialog (2026-10-12). */ const linkedLabel = (t: string, yes: boolean) => `${yes ? 'Yes' : 'No'}, ${t.toLowerCase()} data collected from this app ${yes ? 'is' : 'is not'} linked to the user’s identity`; -const trackingLabel = (t: string, yes: boolean) => (yes ? `Yes, we use ${t.toLowerCase()} data for tracking purposes` : `No, we do not use ${t.toLowerCase()} data for tracking purposes`); +// "Crash Data" -> "crash data", not "crash data" +const dataOf = (t: string) => (/\bdata$/i.test(t) ? t.toLowerCase() : `${t.toLowerCase()} data`); +const trackingLabel = (t: string, yes: boolean) => (yes ? `Yes, we use ${dataOf(t)} for tracking purposes` : `No, we do not use ${dataOf(t)} for tracking purposes`); /** Purpose labels as the console shows them (curly ’ in Developer’s). */ const purposeLabel = (p: string) => p.replace(/'/g, '’'); diff --git a/cli/lib/console-play-plans.ts b/cli/lib/console-play-plans.ts index a9bcd7b..009de09 100644 --- a/cli/lib/console-play-plans.ts +++ b/cli/lib/console-play-plans.ts @@ -45,7 +45,7 @@ export const APP_CONTENT_CARDS: AppContentCard[] = [ { name: 'Sign in details', slug: 'testing-credentials', form: 'play-sign-in-details', via: 'console plan play-sign-in-details', verified: true }, { name: 'Content ratings', slug: 'content-rating-overview', form: 'play-content-rating', via: 'console plan play-content-rating (Start questionnaire > content-rating-iarc-questionnaire)', verified: true }, { name: 'Target audience and content', slug: 'target-audience-content', form: 'play-target-audience', via: 'console plan play-target-audience', verified: true }, - { name: 'Data safety', slug: 'data-privacy-security', form: null, via: 'storebound play data-safety (API; no console form to fill)', verified: false }, + { name: 'Data safety', slug: 'data-privacy-security', form: null, via: 'storebound play data-safety (API; no console form to fill; the page has Export to CSV / Import from CSV)', verified: true }, { name: 'Advertising ID', slug: 'ad-id-declaration', form: 'play-advertising-id', via: 'console plan play-advertising-id', verified: true }, { name: 'Government apps', slug: 'government-apps', form: 'play-government-apps', via: 'console plan play-government-apps', verified: true }, { name: 'Financial features', slug: 'finance', form: 'play-financial-features', via: 'console plan play-financial-features', verified: true }, diff --git a/cli/lib/console-verify.ts b/cli/lib/console-verify.ts index a63e670..f51b130 100644 --- a/cli/lib/console-verify.ts +++ b/cli/lib/console-verify.ts @@ -31,7 +31,7 @@ export interface VerifyResult { notes: string[]; } -const lines = (text: string): string[] => text.normalize('NFC').split(/\r?\n/).map(l => l.replace(/\s+/g, ' ').trim()).filter(Boolean); +const lines = (text: string): string[] => text.normalize('NFC').split(/\r?\n| \| /).map(l => l.replace(/\s+/g, ' ').trim()).filter(Boolean); /** Straight and curly quotes as one, case and spaces aside: for comparing labels. */ export const fold = (s: string): string => s.normalize('NFC').replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, ' ').trim().toLowerCase(); diff --git a/tests/console-plan.test.ts b/tests/console-plan.test.ts index 318e15b..a09b20b 100644 --- a/tests/console-plan.test.ts +++ b/tests/console-plan.test.ts @@ -63,7 +63,7 @@ describe('console plan: the JSON shape for every form', () => { expect(dev).toMatchObject({ label: 'Developer’s Advertising or Marketing', curlyQuotes: true, action: 'tick', status: 'certain', where: 'the "Device ID" dialog, step 1 (purposes)' }); expect(p.answers.find(a => a.id === 'linked:Purchase History')).toMatchObject({ label: 'Yes, purchase history data collected from this app is linked to the user’s identity', action: 'pick' }); expect(p.answers.find(a => a.id === 'tracking:Device ID')).toMatchObject({ label: 'Yes, we use device id data for tracking purposes' }); - expect(p.answers.find(a => a.id === 'tracking:Crash Data')).toMatchObject({ label: 'No, we do not use crash data data for tracking purposes' }); + expect(p.answers.find(a => a.id === 'tracking:Crash Data')).toMatchObject({ label: 'No, we do not use crash data for tracking purposes' }); expect(p.answers.find(a => a.id === 'type:Purchase History')).toMatchObject({ label: 'Purchases', action: 'tick' }); const ph = p.steps.filter(s => s.type === 'Purchase History'); expect(ph.map(s => s.click ?? [])).toEqual([['Set Up Purchase History', 'Set Up Purchases'], ['Next'], ['Next'], ['Next', 'Next'], ['Save']]); @@ -120,6 +120,31 @@ const SAMPLE = [ 'Diagnostics', 'Crash Data', '?', 'Used for App Functionality', 'Not linked to the user’s identity', 'Not used for tracking purposes', 'Edit', ].join('\n'); +/** Real App Privacy page text after publishing (a game, 2026-10-12): the summary sections list category names only; each + * type's lines use Apple's comma style ("X, and Y"), the linked line has a straight apostrophe and is left out when + * not linked; copied text may join a type's lines with " | ". */ +const REAL = [ + 'Data Used to Track You', 'The following data may be used to track you across apps and websites owned by other companies:', 'Identifiers', 'Location', + 'Data Linked to You', 'Location', 'User Content', 'Identifiers', 'Data Not Linked to You', 'Identifiers', + 'Data Types', 'Edit', + 'Coarse Location', "Used for Analytics, App Functionality, and Third-Party Advertising | Linked to the user's identity | Used for tracking purposes", + 'Device ID', 'Used for Third-Party Advertising, and Analytics | Used for tracking purposes', + 'Advertising Data', 'Used for Third-Party Advertising | Used for tracking purposes', + 'Crash Data', "Used for Analytics | Linked to the user's identity", +].join('\n'); + +describe('console verify: App Privacy from the real page text', () => { + test('category names in the summary sections are not types; " | " lines, Apple\'s commas and straight quotes parse', () => { + const m = parseAppPrivacy(REAL); + expect([...m.keys()].sort()).toEqual(['Advertising Data', 'Coarse Location', 'Crash Data', 'Device ID']); + const coarse = m.get('Coarse Location')!; + expect([...coarse.purposes!].sort()).toEqual(['Analytics', 'App Functionality', 'Third-Party Advertising']); + expect(coarse).toMatchObject({ setUp: false, linked: true, tracking: true, inferred: [] }); + expect(m.get('Device ID')).toMatchObject({ purposes: expect.arrayContaining(['Third-Party Advertising', 'Analytics']), linked: false, tracking: true, inferred: ['linked'] }); + expect(m.get('Crash Data')).toMatchObject({ purposes: ['Analytics'], linked: true, tracking: false, inferred: ['tracking'] }); + }); +}); + describe('console verify: App Privacy from the page text', () => { test('parses the per-type summaries (curly quotes, the Purchases label, missing tracking line read as No)', () => { const m = parseAppPrivacy(SAMPLE);