SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-25

Tự host OpenAnalytics trên VPS: yêu cầu và cách cài

OpenAnalytics cần 4 GB RAM, 25 GB đĩa trống, Docker Compose và 4 bản ghi DNS. Xem stack ClickHouse, Postgres, Valkey, cách cài và thứ làm đầy đĩa.

Dấu chân hệ thống trước bước đầu tiên

Để tự host OpenAnalytics, bạn cần một Linux VPS có khoảng 4 GB RAM, 25 GB dung lượng đĩa trống, Docker có Compose plugin và 4 bản ghi DNS đã trỏ đến máy chủ. Đây là điều kiện thực tế cần nêu ngay từ đầu, trước lệnh đầu tiên.

Stack gồm 6 service ứng dụng và 3 kho dữ liệu. Postgres lưu control plane: tài khoản, site, API key và share link. ClickHouse lưu event thô và các dữ liệu tổng hợp mà dashboard đọc. Valkey chạy 2 lần: một instance làm hàng đợi event bền vững và một instance làm cache có thể mất, vì 2 tác vụ này cần chính sách eviction trái ngược nhau. Chỉ một process, query gateway, được phép đọc ClickHouse. Process này xác minh chữ ký Ed25519 trên từng query envelope trước khi thực thi.

Nếu bạn cần một binary và một file cấu hình duy nhất thì đây không phải lựa chọn đó. GoatCounter là tùy chọn single-binary trong nhóm này: một executable Go, dùng SQLite theo mặc định và không cần database bên ngoài. Stack nặng hơn cung cấp funnel, web vitals, revenue attribution từ tài khoản Stripe của bạn và một server MCP (model context protocol). Lựa chọn giữa các công cụ analytics tự host là bài viết phân tích sự đánh đổi này. Hướng dẫn này giả định bạn đã quyết định.

Trỏ 4 bản ghi DNS đến máy chủ trước

4 subdomain phải phân giải về public IP của server trước khi bạn khởi động bất kỳ thành phần nào, vì Caddy yêu cầu chứng chỉ Let's Encrypt ngay lần chạy đầu tiên và challenge sẽ thất bại nếu hostname chưa phân giải được.

  • app.example.com phục vụ dashboard.
  • api.example.com phục vụ API và các OAuth callback.
  • c.example.com phục vụ collector và tracker script.
  • rt.example.com phục vụ realtime stream.

Dùng 4 bản ghi A, hoặc 1 bản ghi A và 3 bản ghi CNAME trỏ đến bản ghi đó. Xác nhận bằng dig +short app.example.com trước khi tiếp tục. Tên miền bạn vừa thêm một phút trước vẫn có thể bị resolver mà Let's Encrypt sử dụng cache dưới dạng NXDOMAIN. Vì vậy, nếu lần yêu cầu chứng chỉ đầu tiên thất bại, hãy chờ và đọc log của Caddy. Chạy lại quá trình cài đặt không làm DNS propagate nhanh hơn.

Cách tự host OpenAnalytics bằng Docker Compose

Checkout một release đã gắn tag. Nhánh mặc định là nơi diễn ra quá trình phát triển, còn release tag mới khớp với các image đã phát hành. Các lệnh bên dưới giả định Docker và Compose plugin đã được cài đặt. Nội dung chạy các service Docker Compose trên VPS hướng dẫn phần này.

git clone https://github.com/OpenLabs-so/openanalytics
cd openanalytics
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./generate-secrets.sh --domain example.com --email you@example.com --with-geoip
docker compose pull && docker compose up -d

sed '/-/d' trong lệnh checkout loại các tag pre-release, nên bạn sẽ chuyển sang phiên bản stable mới nhất thay vì release candidate. --with-geoip tải cơ sở dữ liệu thành phố DB-IP trong quá trình tạo. Nếu bỏ qua bước này, mọi event sẽ có country là null, nên chế độ xem địa lý hoàn toàn không hiển thị dữ liệu. Bạn có thể thêm cơ sở dữ liệu sau bằng cách chạy infra/selfhost/geoip/fetch-dbip.sh, đặt GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb trong env/collector.env, rồi tạo lại collector bằng docker compose up -d --force-recreate collector. Cơ sở dữ liệu này được làm mới hằng tháng, vì vậy hãy fetch lại hằng tháng để dữ liệu thành phố không bị lỗi thời.

Sao lưu các secret đã tạo trước khi tiếp tục

Generator ghi ra ba loại nội dung. .env chứa tên miền và các image reference. env/*.env chứa một file secret cho mỗi service. docker-compose.override.yml chứa ba cặp key Ed25519 dưới dạng YAML block scalar, vì PEM nhiều dòng không thể nằm trong env file. Tất cả đều được git-ignore và không thể tạo lại với cùng giá trị.

Hãy sao chép các file đó ra khỏi máy ngay bây giờ. Mỗi loại mất mát đều có hậu quả cụ thể:

  • Mất password của store, bạn sẽ bị khóa khỏi Postgres và ClickHouse. Chỉ có thể reset từ bên trong các container.
  • Mất OA_CREDENTIAL_KEYRING, mọi credential bên thứ ba đã lưu sẽ không thể khôi phục. Người đã kết nối tài khoản Stripe phải kết nối lại.
  • Mất ANONYMOUS_IDENTITY_SECRET, dữ liệu nhận diện visitor sẽ được tính lại từ đầu: toàn bộ visitor của ngày hôm qua bị tính là visitor mới, và việc gián đoạn sẽ hiện rõ trên biểu đồ.
  • Mất AUTH_SECRET, mọi session bị vô hiệu hóa và tất cả người dùng phải đăng nhập lại.
  • Mất private key dùng để ký, bạn chỉ cần rotate cặp key. Không có dữ liệu nào bị mất.

Hai secret phải giống hệt nhau theo từng byte trong hai file. ANONYMOUS_IDENTITY_SECRET xuất hiện trong collector.env và worker.env vì collector tính visitor hash còn worker ghi hash đó. OA_CREDENTIAL_KEYRING xuất hiện trong api.env và worker.env. Mọi secret khác đều được cố ý giới hạn cho đúng một service. Nếu một service nhận secret mà nó không được phép giữ, service sẽ thoát thay vì khởi động.

Khởi động stack và kiểm tra

grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose ps

migrate áp dụng schema cho Postgres và ClickHouse rồi thoát, vì vậy container migrate ở trạng thái stopped là trạng thái cuối đúng. tracker-build biên dịch oa.js vào một volume được Caddy phục vụ rồi cũng thoát. Các thành phần còn lại phải ở trạng thái healthy trong docker compose ps. Một service liên tục restart gần như luôn bị lỗi khi kiểm tra biến môi trường. Log in tất cả lỗi trong một danh sách thay vì chỉ in một lỗi cho mỗi lần restart. Hai nguyên nhân thường gặp là một biến bị để trống, trường hợp này sẽ bị từ chối thay vì được xem như chưa đặt, và một secret được đặt trong sai file của service.

Trên arm64 hoặc khi dùng một branch, không có image được publish nên bạn phải build cục bộ bằng docker compose up -d --build. Host 4 GB sẽ hết memory giữa chừng trong quá trình build đó. Hãy thêm swap trước. Swap chỉ cần trong lúc build:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Build mất khoảng mười phút. Pull chỉ mất vài phút, đó là lý do các release image được cung cấp.

Tạo tài khoản đầu tiên ngay lập tức

Mở https://app.example.com. Một deployment chưa có ai đăng nhập sẽ không hiển thị biểu mẫu đăng nhập mà cho phép tạo tài khoản đầu tiên. Tài khoản này luôn có quyền cao nhất và là tài khoản duy nhất thấy màn hình cài đặt deployment. Sau khi tài khoản này được tạo, route trả về 409, nên không ai có thể truy cập sau bạn. Hãy làm việc này ngay khi stack hoạt động ổn định, không để sang tuần sau.

Cài tracker

Thêm một site trong dashboard, hệ thống sẽ cung cấp tag cho bạn. Cấu trúc tag là cố định:

<script
  async
  src="https://c.example.com/oa.js"
  data-key="YOUR_TRACKING_KEY"
  data-collector="https://c.example.com"
></script>

Đặt tag này trong phần head của trang. Tracking key được thiết kế để public, nên nó phải nằm trong HTML, nơi bất kỳ ai cũng có thể đọc được. Script cài đặt window.oa. Các lệnh gọi như oa("track", ...) được một stub xếp hàng và đẩy đi khi file tải xong, nên custom event phát sớm sẽ không bị mất. Nếu một thành phần khác trên trang đã dùng window.oa, tracker sẽ cài dưới tên window.openanalytics. Nếu cùng site đó cũng phục vụ một onion service, không đưa tag vào bản build đó. Script tải từ c.example.com sẽ đưa visitor dùng Tor Browser quay lại clearnet và liên kết hai địa chỉ trong cùng một lần tải trang.

Sau đó kiểm tra toàn bộ đường đi từ đầu đến cuối:

curl -s https://c.example.com/oa.js -o /dev/null -w '%{http_code} %{size_download}\n'
curl -s https://api.example.com/health | head -c 200
docker compose logs --tail=50 worker | grep -i batch

Lệnh đầu tiên phải in ra 200 và một vài kilobyte. Tải một trang trên site của bạn, rồi tìm một dòng batch trong log của worker trong vòng vài giây. Collector trả lời 202 ngay khi chấp nhận một event. 202 có nghĩa là đã xếp hàng, không phải đã lưu. Worker là thành phần chuyển event vào ClickHouse. Nếu event được chấp nhận nhưng dashboard không hiển thị gì, worker đang bị chặn. Queue depth của Valkey liên tục tăng sẽ xác nhận điều đó. Nguyên nhân thường gặp là thông tin xác thực ClickHouse trong worker.env sai, hoặc thiếu grant trên một table vừa được migration thêm vào.

Giữ collector public và đặt dashboard sau lớp xác thực

Caddy được tích hợp trong file compose và tự lấy certificate cho cả 4 hostname, nên đường dẫn mặc định không cần bạn cấu hình proxy. Nếu máy chủ đã chạy reverse proxy nginx, hãy đặt stack phía sau infra/selfhost/nginx.conf.example được cung cấp và giữ nguyên cách xử lý header:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header CF-Connecting-IP "";
proxy_set_header True-Client-IP "";
proxy_set_header Fly-Client-IP "";

Collector tạo hash khách truy cập hằng ngày từ IP client, nên phải lấy địa chỉ đó từ connection và không bao giờ lấy từ header. Chuyển tiếp CF-Connecting-IP từ một hop không đáng tin cậy cho phép bất kỳ caller nào tự nhận là bất kỳ địa chỉ nào. Điều này đồng thời làm sai geolocation và thổi phồng số lượng khách truy cập.

Quyền truy cập được phân tách rõ theo hostname. c. và rt. phải cho mọi visitor của mọi site được đo truy cập, nên không được đặt basic auth hoặc IP allowlist phía trước 2 hostname này. app. và api. chỉ cần cho những người đã sign in truy cập. Auth của ứng dụng tự bảo vệ dashboard: password sign-in được bật mặc định thông qua AUTH_PASSWORD_SIGNIN=enabled trong env/api.env, còn các nút Google hoặc GitHub chỉ xuất hiện khi provider tương ứng có cả client ID và client secret. Magic link cần mail transport. Nếu không có mail transport, API chỉ ghi yêu cầu gửi vào outbox, nên không có mail nào được gửi và cũng không có lỗi nào xuất hiện. Nếu các app self-hosted khác của bạn đã nằm sau một lần đăng nhập Authentik duy nhất, hãy quyết định sớm dashboard này sẽ dùng chung hay giữ account riêng, vì account đầu tiên bạn tạo ở đây sẽ vĩnh viễn có quyền privileged.

Một setting quyết định dashboard có hoạt động hay không. AUTH_TRUSTED_ORIGINS trong env/api.env phải khớp chính xác với origin của dashboard. Nếu sai hoặc bị thiếu, API không phát các header CORS (cross-origin resource sharing), browser từ chối mọi call, và bạn sẽ thấy dashboard render layout nhưng không có dữ liệu, trong khi docker compose ps vẫn báo mọi thứ healthy.

Trong lúc chỉnh proxy config, hãy xử lý traffic tự động. Crawler truy cập collector như mọi client khác, và page view của chúng được ghi vào ClickHouse cũng như các số liệu của bạn. Chặn AI crawler tại server giúp loại một phần traffic này khỏi database trước khi nó làm giảm độ chính xác và chiếm disk.

Ý nghĩa của cookieless ở đây và cái giá phải trả

Không có cookie. Danh tính của khách truy cập là một salted hash, salt được xoay vòng mỗi ngày và địa chỉ IP thô không bao giờ được lưu. Vị trí địa lý được xác định cục bộ bằng file DB-IP trên chính disk của bạn, nên không có truy vấn nào về khách truy cập rời khỏi host. Giữ các truy vấn ở local chỉ loại bỏ vendor, không loại bỏ dữ liệu. Đây cũng là giới hạn xuất hiện khi bạn tự chạy một instance SearXNG và IP của server trở thành địa chỉ mà các search engine nhìn thấy.

Lợi ích là không có identifier được lưu trên thiết bị của khách truy cập. Đây chính là yếu tố khiến tracker thuộc phạm vi quy định về consent của ePrivacy tại EU. Vì lý do đó, các hệ thống chỉ dùng dữ liệu tổng hợp như hệ thống này thường được vận hành mà không hiển thị consent banner. GDPR vẫn áp dụng cho mọi dữ liệu bạn lưu và thời gian lưu dữ liệu. Trường hợp của bạn phải do cố vấn pháp lý của bạn quyết định, không phải README.

Cái giá phải trả là không thể xác định danh tính xuyên qua nhiều ngày. Do salt được xoay vòng, một người truy cập vào thứ Hai rồi quay lại vào thứ Tư sẽ được tính là 2 khách truy cập. Đây là hành vi có chủ đích và không có cách khắc phục. Số khách truy cập duy nhất theo ngày là đáng tin cậy. Số khách truy cập duy nhất theo tuần và tháng được tính từ dữ liệu theo ngày nên sẽ phóng đại độ phủ. Vì vậy, mọi chỉ số “khách truy cập quay lại” trong khoảng thời gian dài đều không đo đúng điều mà nhãn của nó mô tả. Sessions và hành trình truy cập đáng tin cậy trong một ngày duy nhất. Việc xoay vòng ANONYMOUS_IDENTITY_SECRET có tác động giống như ranh giới giữa các ngày, nên hãy xem việc xoay vòng đó là một thay đổi dữ liệu, không phải tác vụ vệ sinh định kỳ.

Collector tôn trọng Do Not Track và Global Privacy Control, là browser signal cho website biết không được bán hoặc chia sẻ dữ liệu cá nhân. Thẻ script có các switch riêng cho cùng mục đích: data-respect-gpc, data-respect-dnt và data-require-consent. Switch này giữ toàn bộ hoạt động thu thập cho đến khi consent được cấp và lưu câu trả lời trong localStorage dưới key oa.consent. Thiết lập data-storage="none" sẽ tắt hoàn toàn browser storage.

Vì sao disk đầy sau sáu tháng

Đây là nguyên nhân làm chết một máy chủ analytics tự host, và các event thường không phải nguyên nhân chính.

Bắt đầu với các image. Một release phát hành 10 image, chiếm tổng cộng khoảng 13 GB trên disk. Khi upgrade, hệ thống pull thế hệ mới trước khi xóa thế hệ cũ, nên trong một thời gian máy giữ cả 2 thế hệ. Như vậy đã chiếm phần lớn yêu cầu 25 GB, ngay cả khi chưa có một page view nào.

Tiếp theo là các snapshot. snapshot.sh dừng stack, archive cả 2 data volume cùng với mọi secret, rồi khởi động lại. Bản copy cold là loại duy nhất an toàn trong trường hợp này, vì ClickHouse merge các part ở background và bản copy được tạo trong lúc merge sẽ không nhất quán. upgrade.sh tự động tạo một bản trước mỗi lần upgrade, nên các archive tích lũy trên cùng disk cho đến khi bạn giới hạn số lượng.

./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3

Trên host gần đầy disk, hãy reclaim thế hệ trước khi upgrade. Việc này an toàn khi stack đang chạy, vì các image mà container đang chạy sử dụng vẫn còn được tham chiếu:

docker image prune -a -f

Tiếp theo là chính các event. ClickHouse nén dữ liệu dạng columnar rất mạnh, nên lượng raw event tăng chậm hơn nhiều so với dự đoán của hầu hết mọi người, còn các bảng rollup mà dashboard đọc nhỏ hơn nhiều so với bảng raw. Hãy đo thay vì đoán:

docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse

Để xem dung lượng theo từng bảng, chạy lệnh này bằng credential ClickHouse mà generator đã ghi dưới infra/selfhost/env/:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size, sum(rows) AS row_count
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;

Hãy lấy số liệu đó trong tuần đầu tiên và một lần nữa trong tuần thứ 4. Hai mốc số liệu cho biết tốc độ tăng trưởng, và tốc độ tăng trưởng cho biết khi nào volume cần được resize. Tính đến tháng 8 năm 2026, tài liệu self-hosting không ghi nhận knob retention hoặc time-to-live cho raw event, vì vậy hãy sizing disk dựa trên tốc độ đã đo thay vì giả định các row cũ tự hết hạn.

Có một bẫy khi xóa cần biết trước khi gặp phải. Khi xóa một site hoặc account, hệ thống đưa công việc vào queue cho worker, và worker đó cần CLICKHOUSE_MAINTENANCE_USER cùng CLICKHOUSE_MAINTENANCE_PASSWORD được thiết lập, với một user oa_maintenance tương ứng tồn tại trong ClickHouse. Nếu thiếu các thiết lập này, deletion sẽ nằm trong queue vĩnh viễn. Site biến mất khỏi dashboard nhưng mọi row vẫn còn trên disk, nên bạn có cảm giác đã cleanup nhưng thực tế không lấy lại được dung lượng nào.

Nâng cấp và ba loại chi phí

git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.sh

upgrade.sh in ra ba loại chi phí trước khi thực hiện. Downtime là vấn đề thực tế: các event phát sinh khi collector đang dừng sẽ bị mất vì tracker không retry chúng. Rollback làm mất dữ liệu vì rollback.sh --to backups/<snapshot> thay thế toàn bộ cả hai store và loại bỏ mọi row được ghi sau thời điểm tạo snapshot đó. Disk là chi phí thứ ba, chính là chồng snapshot được mô tả ở trên.

Có hai quy tắc restart rất dễ làm sai. Hãy khởi động query gateway trước API vì API mới hơn sẽ gửi các trường query mà gateway cũ từ chối. ClickHouse cần recreate thay vì restart vì docker compose restart dùng lại environment ban đầu của container và âm thầm bỏ qua thay đổi của bạn:

docker compose up -d --force-recreate clickhouse

Dashboard cũng có kiểu bẫy tương tự. Ba origin NEXT_PUBLIC_* trong env/web.env được biên dịch vào browser bundle và được thay thế khi container khởi động. Vì vậy, nếu dashboard gọi sai hostname, hãy sửa bằng docker compose up -d --force-recreate web, không bao giờ bằng restart. Log của web container in ra các origin mà nó đã khởi động cùng. Đây là cách nhanh nhất để xác nhận thay đổi đã được áp dụng.

Nếu ClickHouse không khởi động sau khi sửa config, hãy đọc dòng đầu tiên trong log. Dòng bắt đầu bằng oa-entrypoint: nghĩa là entrypoint từ chối giá trị bạn đã đặt. Các trường hợp khác thường có nghĩa là file config chứa XML không hợp lệ. Nguyên nhân phổ biến nhất là có dấu gạch ngang kép trong XML comment, điều này không hợp lệ trong XML.

AGPL-3.0 và tên gọi

Mã nguồn được cấp phép theo AGPL-3.0. Việc chạy mã nguồn không sửa đổi cho các site của bạn hoàn toàn không tạo ra nghĩa vụ công bố nào. Nghĩa vụ bắt đầu khi bạn sửa mã nguồn và chạy phiên bản đã sửa đó dưới dạng một network service: khi đó license yêu cầu bạn cung cấp mã nguồn đã sửa cho người dùng của service đó. Điều này bao gồm việc cung cấp dashboard cho client trên instance của bạn và đóng gói mã nguồn vào sản phẩm bạn bán. Giữ các thay đổi trong một public fork là đủ để đáp ứng yêu cầu này mà không cần thêm quy trình nào.

Brand tách biệt với mã nguồn. Tên "OpenAnalytics" và domain hosted của dự án dùng để nhận diện instance do các tác giả vận hành, và không thuộc phạm vi cấp quyền của license. Deployment của bạn chạy phần mềm nhưng không sử dụng brand đó, vì vậy hãy đặt tên riêng cho service trước khi cung cấp service cho khách hàng trả phí.

FAQ

Tôi có thể chạy OpenAnalytics trên VPS 1 GB không?

Không. Dự án yêu cầu khoảng 4 GB RAM và 25 GB dung lượng đĩa trống, vì một deployment chạy sáu application service cùng với Postgres, ClickHouse và hai instance Valkey. Riêng ClickHouse đã không phải là một process nhỏ. Trên máy 1 GB, các container khởi động rồi kernel OOM killer sẽ dừng một container, thường là ClickHouse. Nếu gói 1 GB là giới hạn bắt buộc, hãy dùng công cụ dạng single-binary như GoatCounter. Công cụ này chạy trên SQLite và không cần database bên ngoài.

Đó là vấn đề bạn nên hỏi luật sư. Các yếu tố kỹ thuật hiện có lợi cho bạn. Không có cookie. Danh tính visitor là một salted hash được xoay vòng mỗi ngày. Địa chỉ IP thô không bao giờ được lưu, nên không có dữ liệu bền vững nào được ghi để nhận diện visitor. GDPR vẫn điều chỉnh loại dữ liệu bạn lưu và thời gian lưu giữ. Nếu muốn yêu cầu consent rõ ràng trước khi thu thập, đặt data-require-consent trên script tag. Tracker khi đó không thu thập gì cho đến khi được cấp consent và lưu lựa chọn trong localStorage dưới oa.consent.

Vì sao event trả về 202 nhưng không bao giờ xuất hiện trên dashboard?

202 có nghĩa là collector đã chấp nhận và đưa event vào queue, không có nghĩa là event đã được lưu. Worker lấy event khỏi queue rồi ghi vào ClickHouse, nên dashboard trống dù request thành công cho thấy vấn đề nằm ở worker. Đọc docker compose logs --tail=50 worker và theo dõi độ sâu queue của Valkey. Queue liên tục tăng nghĩa là worker đang bị chặn. Các nguyên nhân thường gặp là thông tin xác thực ClickHouse trong worker.env không đúng hoặc thiếu grant trên một table được tạo trong migration gần đây.

Vì sao dashboard trống dù mọi container đều healthy?

Trước tiên, kiểm tra AUTH_TRUSTED_ORIGINS trong env/api.env. Giá trị này phải khớp chính xác với origin của dashboard. Nếu không khớp, API sẽ không phát CORS header, nên browser từ chối mọi call. Khi đó bạn sẽ thấy layout hoạt động nhưng không có dữ liệu. Việc thứ hai cần kiểm tra là ba giá trị NEXT_PUBLIC_* trong env/web.env. Các giá trị này được thay thế khi web container khởi động. Sửa chúng cần docker compose up -d --force-recreate web, vì restart thông thường vẫn giữ các giá trị cũ.

AGPL-3.0 có ngăn tôi cung cấp phần mềm này cho client không?

Không, nhưng license này có một điều kiện. Nếu chạy code không chỉnh sửa, bạn không phải cung cấp gì cho bất kỳ ai. Nếu sửa code rồi chạy phiên bản đã sửa đó dưới dạng service cho người khác sử dụng, bạn phải cung cấp source đã sửa cho những người dùng đó. Một public fork đáp ứng yêu cầu này. Ngoài ra, tên "OpenAnalytics" không được cấp phép cùng với code, nên mọi sản phẩm bạn bán cần có tên riêng.