From 58f50647ba85aa9ff4d4463a372484582276e878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Ya=C4=9F=C4=B1z?= Date: Mon, 19 Oct 2026 21:32:55 +0300 Subject: [PATCH 1/2] feat(privacy): drop Google Analytics in favor of Cloudflare Web Analytics Google Analytics loaded on every page before any choice and set tracking cookies, which the consent banner and the policies said did not exist. Traffic is now measured only by Cloudflare Web Analytics, which is cookieless; its beacon was already allowed by the CSP. Co-Authored-By: Claude Opus 5.5 --- src/frontend/nginx-security-headers.conf | 6 +- src/frontend/src/app/app.ts | 3 - .../app/core/services/analytics.service.ts | 56 ------------------- .../src/environments/environment.prod.ts | 3 +- src/frontend/src/environments/environment.ts | 3 +- 5 files changed, 5 insertions(+), 66 deletions(-) delete mode 100644 src/frontend/src/app/core/services/analytics.service.ts diff --git a/src/frontend/nginx-security-headers.conf b/src/frontend/nginx-security-headers.conf index 762bd57..3ac344b 100644 --- a/src/frontend/nginx-security-headers.conf +++ b/src/frontend/nginx-security-headers.conf @@ -9,6 +9,6 @@ add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment # No 'unsafe-eval': nothing in the application or the Hangfire dashboard evaluates strings. # 'unsafe-inline' stays for scripts: the theme script in index.html, Angular's event replay and its # stylesheet loader are inline. Cloudflare Web Analytics injects its beacon from static.cloudflareinsights.com -# and reports to cloudflareinsights.com. connect-src lists the API (same origin, the hub included), -# that beacon and Google Analytics; nothing else is called from a browser. -add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com https://*.google-analytics.com https://*.analytics.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; +# and reports to cloudflareinsights.com. connect-src lists the API (same origin, the hub included) +# and that beacon; nothing else is called from a browser. +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; diff --git a/src/frontend/src/app/app.ts b/src/frontend/src/app/app.ts index 2e071c0..c61dfc9 100644 --- a/src/frontend/src/app/app.ts +++ b/src/frontend/src/app/app.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, afterNextRender, effect, inject, un import { RouterOutlet } from '@angular/router'; import { ToastComponent } from './shared/components/toast/toast'; import { ConfirmModalComponent } from './shared/components/confirm-modal/confirm-modal.component'; -import { AnalyticsService } from './core/services/analytics.service'; import { RouteLoaderComponent } from './shared/components/route-loader/route-loader'; import { PackageService } from './core/services/package.service'; import { SignalrService } from './core/services/signalr.service'; @@ -22,8 +21,6 @@ export class App { private liveUpdates = inject(LiveUpdatesService); constructor() { - inject(AnalyticsService).init(); - // One hub connection and one feed for every page, in the browser only afterNextRender(() => { this.loadLiveUpdates(); diff --git a/src/frontend/src/app/core/services/analytics.service.ts b/src/frontend/src/app/core/services/analytics.service.ts deleted file mode 100644 index 8d59fd7..0000000 --- a/src/frontend/src/app/core/services/analytics.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, PLATFORM_ID, inject } from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; -import { Router, NavigationEnd } from '@angular/router'; -import { filter } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; - -type GtagArguments = [command: string, ...parameters: unknown[]]; - -interface AnalyticsWindow { - dataLayer?: unknown[]; - gtag?: (...args: GtagArguments) => void; -} - -@Injectable({ providedIn: 'root' }) -export class AnalyticsService { - private router = inject(Router); - private isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); - - // Lives as long as the application, so the subscription is never torn down - init(): void { - if (!this.isBrowser || !environment.gaId) return; - - const gtag = this.loadScript(environment.gaId); - - this.router.events.pipe( - filter((event): event is NavigationEnd => event instanceof NavigationEnd) - ).subscribe(event => { - if (event.urlAfterRedirects.startsWith('/admin')) return; - - gtag('event', 'page_view', { - page_path: event.urlAfterRedirects, - page_location: window.location.href, - page_title: document.title - }); - }); - } - - private loadScript(gaId: string): (...args: GtagArguments) => void { - const script = document.createElement('script'); - script.async = true; - script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`; - document.head.appendChild(script); - - const analytics = window as unknown as AnalyticsWindow; - const dataLayer = analytics.dataLayer ?? (analytics.dataLayer = []); - // gtag.js reads the queue entries as the arguments objects the snippet pushes, not as arrays - const gtag = function (..._args: GtagArguments) { - dataLayer.push(arguments); - }; - analytics.gtag = gtag; - - gtag('js', new Date()); - gtag('config', gaId, { send_page_view: false }); - return gtag; - } -} diff --git a/src/frontend/src/environments/environment.prod.ts b/src/frontend/src/environments/environment.prod.ts index ec78052..45aabfc 100644 --- a/src/frontend/src/environments/environment.prod.ts +++ b/src/frontend/src/environments/environment.prod.ts @@ -1,6 +1,5 @@ export const environment = { production: true, apiUrl: '/api', - hubUrl: '/analysisHub', - gaId: 'G-TBR2L3QF2R' + hubUrl: '/analysisHub' }; diff --git a/src/frontend/src/environments/environment.ts b/src/frontend/src/environments/environment.ts index 35e9cd3..c7959d6 100644 --- a/src/frontend/src/environments/environment.ts +++ b/src/frontend/src/environments/environment.ts @@ -1,6 +1,5 @@ export const environment = { production: false, apiUrl: 'http://localhost:5054/api', - hubUrl: 'http://localhost:5054/analysisHub', - gaId: '' // GA4 tracking disabled in development + hubUrl: 'http://localhost:5054/analysisHub' }; From f5da5277ee1183aa9bb4df7b592df4e362cd6637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Ya=C4=9F=C4=B1z?= Date: Mon, 19 Oct 2026 21:32:55 +0300 Subject: [PATCH 2/2] docs(privacy): rewrite the privacy, cookie and terms pages to match what is processed The privacy policy now names the controller, the legal basis (GDPR and KVKK), concrete retention periods, the processors (Cloudflare, hosting, Google Fonts, GitHub) and the rights including a complaint to a supervisory authority. The cookie policy lists every browser storage key, and the consent banner becomes a notice, since nothing needs consent. Co-Authored-By: Claude Opus 5.5 --- .../src/app/features/cookies/cookies.html | 47 ++-- .../src/app/features/cookies/cookies.ts | 2 +- .../src/app/features/privacy/privacy.html | 231 +++++++++++++----- .../src/app/features/privacy/privacy.ts | 5 +- .../src/app/features/terms/terms.html | 11 +- src/frontend/src/app/features/terms/terms.ts | 2 +- .../cookie-consent/cookie-consent.html | 20 +- .../cookie-consent/cookie-consent.ts | 12 +- 8 files changed, 223 insertions(+), 107 deletions(-) diff --git a/src/frontend/src/app/features/cookies/cookies.html b/src/frontend/src/app/features/cookies/cookies.html index 35a1b29..3bc67ce 100644 --- a/src/frontend/src/app/features/cookies/cookies.html +++ b/src/frontend/src/app/features/cookies/cookies.html @@ -14,9 +14,9 @@

1. Overview

- {{ appName }} uses browser local storage to enhance your user experience. We do not use - third-party cookies or tracking technologies. This policy explains what we store locally - in your browser and why. + {{ appName }} sets no cookies and uses no tracking technologies. It keeps a few preferences in + your browser's local storage so the site remembers them. This policy lists exactly what is + stored and why.

@@ -24,8 +24,8 @@

1. Overvie

2. What We Store

- All data is stored locally in your browser using localStorage. Nothing is sent to - external servers except when explicitly requested by your actions (e.g., package searches). + These entries live only in your browser (localStorage, or sessionStorage where noted). They are + never sent to our servers or to anyone else.

@@ -52,6 +52,12 @@

2. What We Essential Token lifetime + + lastSelectedRegistry + The registry you last selected on the home page + Functional + Persistent + recentRegistries Recent registry selections for quick access @@ -60,26 +66,38 @@

2. What We pkgdiff-cookie-consent - Your cookie consent choice + Remembers that you dismissed the privacy notice Essential Persistent + + pkgdiff-linkedin-oauth-state + Protects the LinkedIn sign-in flow (admin only, sessionStorage) + Essential + Until the flow completes +

- All items marked as "Functional" or "Essential" are necessary for the Service to operate - correctly. We do not use analytics, advertising, or tracking cookies. + "Essential" entries are needed for the Service to work; "Functional" entries only remember your + choices. None of them identify or track you, so they need no consent.

-

3. Third-Party Cookies

+

3. Analytics and Third Parties

+

+ We count visits with Cloudflare Web Analytics, which sets no cookies, stores nothing in your + browser and does not track you across sites or visits. +

- {{ appName }} does not integrate with any third-party services that set cookies or track - users. All functionality is self-contained within our application. + No third-party service sets cookies on {{ appName }}. Your browser does contact Google Fonts and + GitHub to load fonts and images; see the + Privacy Policy + for what they receive.

@@ -95,8 +113,8 @@

4. Managin
  • Safari: Preferences → Privacy → Manage Website Data → pkgdiff.dev → Remove
  • - Clearing local storage will reset your theme preference and consent choice. Essential - functional data (like auth tokens) will also be removed, logging you out if applicable. + Clearing it resets your theme and registry choices and shows the privacy notice again. + Administrators are also signed out.

    @@ -105,8 +123,7 @@

    4. Managin

    5. Changes to This Policy

    We may update this Cookie Policy from time to time. Changes will be posted on this page - with an updated "Last updated" date. Continued use of {{ appName }} after changes - constitutes acceptance of the updated policy. + with an updated "Last updated" date.

    diff --git a/src/frontend/src/app/features/cookies/cookies.ts b/src/frontend/src/app/features/cookies/cookies.ts index 0d60ce9..99d4fcf 100644 --- a/src/frontend/src/app/features/cookies/cookies.ts +++ b/src/frontend/src/app/features/cookies/cookies.ts @@ -12,7 +12,7 @@ export class CookiesComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 9, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit() { this.seo.updateMeta({ diff --git a/src/frontend/src/app/features/privacy/privacy.html b/src/frontend/src/app/features/privacy/privacy.html index 6e52b35..7cc751e 100644 --- a/src/frontend/src/app/features/privacy/privacy.html +++ b/src/frontend/src/app/features/privacy/privacy.html @@ -14,120 +14,227 @@

    1. Overview

    - {{ appName }} is committed to protecting your privacy. This policy explains what information we - collect, how we use it, and your rights regarding that information. We designed the Service to - minimize data collection — we only collect what is necessary to operate and improve {{ appName }}. + This policy explains what personal data {{ appName }} ("the Service", "we") processes when you + visit {{ siteUrl }}, why, for how long, and which rights you have. {{ appName }} needs no account + to use, sets no tracking cookies and shows no ads. We keep only what is needed to run the Service + and protect it from abuse.

    - +
    -

    2. Information We Collect

    +

    2. Who Is Responsible

    +

    + The data controller is {{ controllerName }}, the individual developer who operates {{ appName }} + (based in {{ controllerCountry }}). You can reach the controller about anything in this policy at + {{ contactEmail }}. +

    +
    + + +
    +

    3. Information We Process

    + +

    3.1 Request Logs

    +

    + For every request to our API we record your IP address, your browser's user agent string, the + requested path, the HTTP method, the response status and the time. We use these logs to detect + abuse, enforce rate limits and diagnose errors. +

    + +

    3.2 Search Data

    +

    + When you compare package versions, we store the package name, the selected versions, + whether prereleases were included, the time, how long the analysis took and your IP address. + Package names and versions feed the public "recent" and "popular" lists; your IP address is never + shown publicly and is used only to spot automated abuse. +

    + +

    3.3 Rate Limiting and Blocking

    +

    + Short-lived counters keyed by IP address enforce rate limits and expire on their own. An IP address + that attacks or overloads the Service can be blocked, temporarily or permanently, together with the + reason for the block. +

    -

    2.1 Search Data

    +

    3.4 Analytics

    - When you search for a package, we store the package name, selected versions, and a timestamp - of the search. This data is used to display recent searches and improve the Service. + We measure traffic with Cloudflare Web Analytics. It sets no cookies, uses no browser storage and + does not fingerprint or follow individual visitors. It reports aggregated data such as the page + visited, the referring site, browser and device type, country and page load performance.

    -

    2.2 Technical Data

    +

    3.5 Administrator Accounts

    - We may collect your IP address, browser type, and general usage patterns through server logs. - This information is used for security monitoring, rate limiting, and diagnosing technical issues. + There is no public sign-up. The only accounts are those of the Service's administrators, for whom + we store a username, an email address and a hashed password, never the password itself.

    -

    2.3 Account Data

    +

    3.6 Email

    - If you create an account, we store your email address and an encrypted password. We never store - passwords in plain text. + If you write to us, we use your email address and message only to answer you.

    - +
    -

    3. What We Don't Collect

    +

    4. Why We Process It (Legal Basis)

      -
    • We do not use third-party tracking or advertising cookies
    • -
    • We do not sell, rent, or share your personal data with third parties
    • -
    • We do not collect financial or payment information
    • -
    • We do not track your browsing activity outside of {{ appName }}
    • +
    • Request logs, search data, rate limiting, blocking and analytics: our legitimate interest in + running a secure, reliable and free Service and understanding how it is used (Art. 6(1)(f) GDPR; + Art. 5(2)(f) of Turkish Law No. 6698 on the Protection of Personal Data, "KVKK").
    • +
    • Administrator accounts and email correspondence: our legitimate interest in operating the Service + and answering the people who contact us (the same provisions).
    +

    + We do not sell your data, use it for advertising, build profiles of you, or make automated + decisions about you that have legal or similarly significant effects. +

    - +
    -

    4. How We Use Your Data

    -
      -
    • To provide and maintain the Service
    • -
    • To display recent and popular package searches
    • -
    • To protect against abuse and enforce rate limits
    • -
    • To diagnose and fix technical issues
    • -
    • To improve the Service based on usage patterns
    • -
    +

    5. How Long We Keep It

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DataRetention
    Request logsDeleted automatically after 30 days
    Search dataDeleted automatically after 90 days
    Rate-limit countersExpire on their own within minutes
    Temporary IP blocksRemoved once the block expires
    Permanent IP blocksKept until an administrator lifts the block or you ask us to review it
    Database backupsOverwritten after 14 days
    Server and container logsRotated automatically and overwritten after a short period
    Administrator accountsUntil the account is removed
    Email correspondenceAs long as needed to handle your request
    +
    - +
    -

    5. Data Storage & Retention

    -

    - Data is stored on secured servers. Cached package data is automatically purged after 30 days - and refreshed from upstream sources. Search history is retained for operational purposes and may - be periodically cleaned. You can request deletion of your data by contacting us. +

    6. Service Providers and International Transfers

    +

    We share personal data only with the providers needed to run the Service:

    +
      +
    • Cloudflare, Inc. — every request to {{ appName }} passes through Cloudflare for DNS, + TLS, caching, DDoS protection and Web Analytics, so Cloudflare processes your IP address and + request details on our behalf.
    • +
    • Our server hosting provider — the virtual server that runs the application, + its database and its backups.
    • +
    • Google Fonts — the site's fonts are loaded from Google's servers, which receive + your IP address and browser details when your browser downloads them.
    • +
    • GitHub and other content hosts — contributor avatars and images embedded in + release notes are loaded by your browser directly from where they are hosted, mostly GitHub.
    • +
    +

    + Cloudflare, Google and GitHub may process data outside your country, including in the United + States. Cloudflare processes it under its Data Processing Addendum, which includes the European + Commission's Standard Contractual Clauses.

    -
    - - -
    -

    6. Third-Party Services

    -

    - {{ appName }} retrieves data from NuGet.org, npmjs.com, and GitHub to display package information. - When you search for a package, we make requests to these services on your behalf. These services - have their own privacy policies that govern how they handle data. +

    + To build release notes, our server fetches public data from NuGet.org, npmjs.com, PyPI, pub.dev and + GitHub, and security data from sources such as OSV and deps.dev. These requests come from our server + and contain package names and versions only — never your IP address or any other data about you.

    7. Your Rights

    +

    + Under the GDPR, the KVKK (Article 11) and similar laws, you can: +

      -
    • Request access to the personal data we hold about you
    • -
    • Request correction or deletion of your data
    • -
    • Object to processing of your data
    • -
    • Request a copy of your data in a portable format
    • +
    • Ask whether we process data about you and get a copy of it
    • +
    • Ask us to correct or delete it
    • +
    • Object to processing based on our legitimate interests
    • +
    • Ask us to restrict processing, or to hand your data over in a portable format
    • +
    • Learn who we have shared it with
    • +
    • Lodge a complaint with a supervisory authority — in Türkiye the Personal Data Protection + Authority (KVKK), in the EU the data protection authority of your country

    - To exercise any of these rights, contact us at + To use any of these rights, email {{ contactEmail }}. + Most of our records are keyed only by IP address, so please include the IP address(es) and the + approximate dates concerned. We answer within 30 days.

    - +
    -

    8. Changes to This Policy

    +

    8. Security

    - We may update this Privacy Policy from time to time. Changes will be posted on this page with - an updated revision date. Continued use of the Service after changes constitutes acceptance - of the revised policy. + Traffic is encrypted with TLS, the database is reachable only from inside the server, administrative + areas sit behind additional access controls, and backups are readable only by the server's owner.

    - +
    -

    9. Related Policies

    -

    - For complete transparency, please also review: +

    9. Children

    +

    + {{ appName }} is a developer tool and is not directed at children under 16. +

    +
    + + +
    +

    10. Changes to This Policy

    +

    + We may update this policy when the Service changes. The new version is posted on this page with + an updated date.

    +
    + + +
    +

    11. Related Policies

    - +
    -

    10. Contact

    +

    12. Contact

    - If you have questions about this Privacy Policy, please contact us at + Questions about this policy or your data: {{ contactEmail }}.

    diff --git a/src/frontend/src/app/features/privacy/privacy.ts b/src/frontend/src/app/features/privacy/privacy.ts index 2f95968..7657f7e 100644 --- a/src/frontend/src/app/features/privacy/privacy.ts +++ b/src/frontend/src/app/features/privacy/privacy.ts @@ -13,7 +13,10 @@ export class PrivacyComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 8, 2026'; + siteUrl = 'pkgdiff.dev'; + controllerName = 'Enes Yağız'; + controllerCountry = 'Türkiye'; + lastUpdated = 'October 19, 2026'; ngOnInit(): void { this.seo.updateMeta({ diff --git a/src/frontend/src/app/features/terms/terms.html b/src/frontend/src/app/features/terms/terms.html index 0c84aea..8a5a270 100644 --- a/src/frontend/src/app/features/terms/terms.html +++ b/src/frontend/src/app/features/terms/terms.html @@ -25,7 +25,7 @@

    1. Accepta

    2. Description of Service

    {{ appName }} is a free, open-source tool that allows users to compare package versions across - registries (such as NuGet and npm), view release notes, and analyze differences between versions. + registries (NuGet, npm, PyPI and pub.dev), view release notes, and analyze differences between versions. The Service retrieves publicly available data from package registries and GitHub repositories.

    @@ -48,9 +48,10 @@

    3. Accepta

    4. Data & Content

    The release notes, version information, and package metadata displayed on {{ appName }} are sourced - from third-party registries (NuGet.org, npmjs.com) and GitHub. We do not claim ownership of this - content. The accuracy, completeness, and timeliness of this data depend on the upstream sources. - {{ appName }} is not responsible for errors or omissions in third-party data. + from third-party registries (NuGet.org, npmjs.com, PyPI and pub.dev) and GitHub. We do not claim + ownership of this content; it belongs to its authors and is shown with a link to its source. The accuracy, completeness, and timeliness of this data depend on the upstream sources. + {{ appName }} is not responsible for errors or omissions in third-party data. If you own content + shown here and want it corrected or removed, contact us and we will act on it promptly.

    @@ -58,7 +59,7 @@

    4. Data &a

    5. Intellectual Property

    - The {{ appName }} source code is available under its respective open-source license. The Service's + The {{ appName }} source code is available under the MIT License. The Service's branding, design, and original content are the property of the {{ appName }} project. Third-party package names, logos, and trademarks belong to their respective owners.

    diff --git a/src/frontend/src/app/features/terms/terms.ts b/src/frontend/src/app/features/terms/terms.ts index c07e431..17e24ab 100644 --- a/src/frontend/src/app/features/terms/terms.ts +++ b/src/frontend/src/app/features/terms/terms.ts @@ -13,7 +13,7 @@ export class TermsComponent implements OnInit { private seo = inject(SeoService); appName = APP_CONFIG.name; contactEmail = APP_CONFIG.contact.email; - lastUpdated = 'February 8, 2026'; + lastUpdated = 'October 19, 2026'; ngOnInit(): void { this.seo.updateMeta({ diff --git a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html index ecdf7d0..96ab46a 100644 --- a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html +++ b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.html @@ -7,12 +7,13 @@

    - We use local storage to enhance your experience. No third-party cookies or tracking. - - Learn more + Privacy Policy

    @@ -20,18 +21,11 @@
    -
    diff --git a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts index 82d7289..a47dd42 100644 --- a/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts +++ b/src/frontend/src/app/shared/components/cookie-consent/cookie-consent.ts @@ -19,23 +19,17 @@ export class CookieConsentComponent { constructor() { const destroyRef = inject(DestroyRef); - // Only a browser knows whether the visitor has already chosen + // Only a browser knows whether the visitor has already dismissed the notice afterNextRender(() => { if (this.storage.get(CONSENT_KEY)) return; - // Show banner after a short delay for better UX const timer = setTimeout(() => this.visible.set(true), 1000); destroyRef.onDestroy(() => clearTimeout(timer)); }); } - accept() { - this.storage.set(CONSENT_KEY, 'accepted'); - this.visible.set(false); - } - - decline() { - this.storage.set(CONSENT_KEY, 'declined'); + dismiss() { + this.storage.set(CONSENT_KEY, 'seen'); this.visible.set(false); } }