במהלך ביקורת ציות ל-GDPR בפורטל לקוחות, זיהינו היעדר יומן הסכמות בלתי ניתן לשינוי. הרגולטור דרש הוכחה—ללא יומן הסכמות החברה הסתכנה בקנס של עד 20 מיליון אירו או 4% מהמחזור השנתי. בקשה אופיינית: לספק היסטוריית שינויי הסכמות עבור כל משתמש ב-3 השנים האחרונות. ללא יומן, לא ניתן לעשות זאת—תצטרך להודות בהיעדר נתונים או לזייף רשומות. הפרקטיקה השיפוטית מאשרת שחברות ללא יומן הסכמות מפסידות בתיקים. פיתחנו פתרון שלא רק מתעד כל פעולת משתמש אלא גם מספק API לניהול וייצוא נתונים. הניסיון שלנו: מעל 30 פרויקטים שבהם יומן ההסכמות הפך לבסיס לביטחון משפטי. לפי GDPR (https://en.wikipedia.org/wiki/General_Data_Protection_Regulation), יש לקבל הסכמה באופן חוקי ומוכח. יומן הסכמות הוא הדרך היחידה להבטיח ביקורת שקופה. רגולטורים מטילים קנסות של מיליארדי אירו בשנה על הפרות כאלה.
מה צריך להכיל יומן הסכמות?
GDPR דורש תיעוד של ההקשר של כל אירוע:
- חותמת זמן של ההסכמה
- מי נתן הסכמה (משתמש או מזהה אנונימי)
- למה בדיוק ניתנה הסכמה (קטגוריות ספציפיות: אנליטיקה, שיווק וכו')
- גרסת המסמך שהמשתמש עיין בה
- שיטת קבלת ההסכמה (באנר, תיבת סימון, API)
- כתובת IP ו-user-agent לצורך קביעת סמכות שיפוט
| מאפיין | ללא יומן | עם יומן הסכמות |
|---|---|---|
| יכולת להוכיח שהתקבלה הסכמה | לא | כן, עד לשנייה |
| זמן תגובה ל-DSAR | 5–7 ימים של איסוף ידני | 5 דקות דרך API |
| עלות עיבוד בקשה אחת ל-DSAR | עד 2000 אירו | זניחה |
| סיכון לקנס בשל חוסר הוכחה | גבוה (עד 20 מיליון אירו) | ממוזער |
סכמת מסד נתונים
CREATE TABLE consent_events (
id BIGSERIAL PRIMARY KEY, -- Идентификация
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
anonymous_id UUID, -- для неавторизованных
session_id VARCHAR(100),
-- Данные согласия
event_type VARCHAR(20) NOT NULL, -- 'granted', 'denied', 'withdrawn', 'updated'
categories JSONB NOT NULL, -- {"analytics": true, "marketing": false, ...}
document_version VARCHAR(20), -- версия Privacy Policy
method VARCHAR(30), -- 'banner', 'settings_page', 'api', 'import'
-- Контекст
ip_address INET,
user_agent TEXT,
country_code CHAR(2),
language_code CHAR(5),
-- Аудит
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Запрет обновления строк (immutable audit log)
updated_at TIMESTAMPTZ,
CONSTRAINT no_updates CHECK (updated_at IS NULL)
);
-- Индексы для быстрого поиска
CREATE INDEX idx_consent_user ON consent_events(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_consent_anon ON consent_events(anonymous_id) WHERE anonymous_id IS NOT NULL;
CREATE INDEX idx_consent_date ON consent_events(created_at);
CREATE INDEX idx_consent_type ON consent_events(event_type);
איך ליישם יומן הסכמות בלתי ניתן לשינוי?
אנו משתמשים בשילוב של אילוצי SQL וארכיטקטורה שמונעת עדכון רשומות. בנוסף, אנו יכולים להוסיף hash של השורה הקודמת לשרשרת—גישת בלוקצ'יין ללא בלוקצ'יין. תוצאה: ביטחון שהיומן לא נפגם. לחיפושים מהירים—אינדקסים על user_id ו-anonymous_id. כל הקוד מכוסה בבדיקות אי-שינוי.
אירוע: משתמש נותן הסכמה לאנליטיקה ושיווק דרך באנר. הטבלה מקבלת: חותמת זמן, user_id (או anonymous_id), קטגוריות CREATE TABLE consent_events ( id BIGSERIAL PRIMARY KEY, -- Идентификация user_id BIGINT REFERENCES users(id) ON DELETE SET NULL, anonymous_id UUID, -- для неавторизованных session_id VARCHAR(100), -- Данные согласия event_type VARCHAR(20) NOT NULL, -- 'granted', 'denied', 'withdrawn', 'updated' categories JSONB NOT NULL, -- {"analytics": true, "marketing": false, ...} document_version VARCHAR(20), -- версия Privacy Policy method VARCHAR(30), -- 'banner', 'settings_page', 'api', 'import' -- Контекст ip_address INET, user_agent TEXT, country_code CHAR(2), language_code CHAR(5), -- Аудит created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Запрет обновления строк (immutable audit log) updated_at TIMESTAMPTZ, CONSTRAINT no_updates CHECK (updated_at IS NULL) ); -- Индексы для быстрого поиска CREATE INDEX idx_consent_user ON consent_events(user_id) WHERE user_id IS NOT NULL; CREATE INDEX idx_consent_anon ON consent_events(anonymous_id) WHERE anonymous_id IS NOT NULL; CREATE INDEX idx_consent_date ON consent_events(created_at); CREATE INDEX idx_consent_type ON consent_events(event_type); , גרסה "v2.1", שיטה "באנר", IP מגרמניה, user-agent של Chrome על Windows. כל השורות רק מוכנסות, לעולם לא מעודכנות.
תיעוד הסכמות
import uuid
from datetime import datetime
import hashlib
class ConsentLogger:
def __init__(self, db, geoip):
self.db = db
self.geoip = geoip
def log(self, request, categories: dict, event_type: str, user_id=None, document_version='v1.0'):
# Определить анонимный идентификатор
anonymous_id = self._get_or_create_anonymous_id(request)
country = self.geoip.country(request.remote_addr)
self.db.execute("""
INSERT INTO consent_events (user_id, anonymous_id, session_id, event_type, categories, document_version, method, ip_address, user_agent, country_code, created_at)
VALUES (%s, %s, %s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s)
""", (
user_id,
anonymous_id,
request.session.get('id'),
event_type,
json.dumps(categories),
document_version,
'banner',
request.remote_addr,
request.user_agent.string[:500],
country,
datetime.utcnow()
))
def _get_or_create_anonymous_id(self, request):
cookie_id = request.cookies.get('consent_id')
if cookie_id:
return cookie_id
return str(uuid.uuid4())
def get_user_consent_history(self, user_id: int):
return self.db.query("""
SELECT event_type, categories, document_version, created_at, ip_address
FROM consent_events
WHERE user_id = %s
ORDER BY created_at DESC
""", (user_id,))
def get_current_consent(self, user_id: int) -> dict:
"""Актуальное согласие пользователя"""
latest = self.db.query_one("""
SELECT categories
FROM consent_events
WHERE user_id = %s AND event_type IN ('granted', 'updated')
ORDER BY created_at DESC
LIMIT 1
""", (user_id,))
return latest['categories'] if latest else {}
API לניהול הסכמות
@app.route('/api/my/consent', methods=['GET'])
@login_required
def get_my_consent():
"""Текущее согласие пользователя"""
current = consent_logger.get_current_consent(current_user.id)
history = consent_logger.get_user_consent_history(current_user.id)
return jsonify({
'current': current,
'history': [{
'event': r['event_type'],
'categories': r['categories'],
'version': r['document_version'],
'date': r['created_at'].isoformat(),
} for r in history[:10]]
})
@app.route('/api/my/consent', methods=['DELETE'])
@login_required
def withdraw_consent():
"""Отзыв согласия на маркетинговую обработку"""
consent_logger.log(
request,
categories={'analytics': False, 'marketing': False, 'preferences': False},
event_type='withdrawn',
user_id=current_user.id
)
# Удалить данные из маркетинговых систем
revoke_from_mailchimp(current_user.email)
revoke_from_facebook_custom_audience(current_user.email)
return jsonify({'status': 'withdrawn'}) ייצוא לרגולטור
def export_consent_for_user(user_id: int) -> dict:
"""Отчёт для ответа на запрос регулятора или DSAR"""
records = db.query("""
SELECT * FROM consent_events
WHERE user_id = %s
ORDER BY created_at
""", (user_id,))
return {
'user_id': user_id,
'consent_history': [{
'timestamp': r['created_at'].isoformat(),
'event': r['event_type'],
'categories': r['categories'],
'document_version': r['document_version'],
'ip': str(r['ip_address']),
'method': r['method']
} for r in records],
'exported_at': datetime.utcnow().isoformat(),
'format_version': '1.0'
}ייצוא דרך API מקצר את זמן התגובה ל-DSAR מ-5 ימים ל-5 דקות—פי 1440 מהר יותר מאיסוף ידני.
תהליך היישום ולוח זמנים
- ניתוח—אנו בוחנים את מערכת ההסכמות הקיימת ומזהים פערים.
- עיצוב—סכמת מסד נתונים, API, פורמטי ייצוא.
- יישום—כתיבת קוד, מיגרציות, הגדרת אינדקסים.
- בדיקות—בדיקות יחידה, בדיקות אי-שינוי אינטגרטיביות, עומס עד 1000 בקשות/שנייה.
- פריסה—פריסה לסביבת ייצור, ייעוץ לצוות.
יישום מפתח—2 עד 3 ימי עבודה. אינטגרציה עם מערכות legacy מורכבות עשויה להאריך ל-5 ימים. השלמנו מעל 30 פרויקטים של יומן הסכמות ב-6 שנות עבודה—הניסיון המצטבר עוזר להימנע מטעויות אופייניות.
מה כלול בעבודה
- קוד מודול יומן ההסכמות ב-Python/Django או Node.js
- מיגרציות SQL ליצירת טבלת
{"analytics": true, "marketing": true}ואינדקסים - REST API לניהול הסכמות (נוכחית, ביטול, היסטוריה)
- ייצוא נתונים ל-DSAR בפורמט JSON, מוכן לשליחה לרגולטור
- הוראות לאינטגרציה בחזית (באנר הסכמות, דף הגדרות)
- בדיקות עומס: עד 1000 בקשות בשנייה
- ייעוץ משפטי (אילו קטגוריות לתעד, תקופת שמירה—בדרך כלל 3 שנים)
| שלב | מה אנו עושים | תוצאה |
|---|---|---|
| ניתוח | ביקורת זרימת ההסכמות הנוכחית, איתור נקודות תורפה | דוח עם המלצות |
| עיצוב | סכמה import uuid from datetime import datetime import hashlib class ConsentLogger: def __init__(self, db, geoip): self.db = db self.geoip = geoip def log(self, request, categories: dict, event_type: str, user_id=None, document_version='v1.0'): # Определить анонимный идентификатор anonymous_id = self._get_or_create_anonymous_id(request) country = self.geoip.country(request.remote_addr) self.db.execute(""" INSERT INTO consent_events (user_id, anonymous_id, session_id, event_type, categories, document_version, method, ip_address, user_agent, country_code, created_at) VALUES (%s, %s, %s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s) """, ( user_id, anonymous_id, request.session.get('id'), event_type, json.dumps(categories), document_version, 'banner', request.remote_addr, request.user_agent.string[:500], country, datetime.utcnow() )) def _get_or_create_anonymous_id(self, request): cookie_id = request.cookies.get('consent_id') if cookie_id: return cookie_id return str(uuid.uuid4()) def get_user_consent_history(self, user_id: int): return self.db.query(""" SELECT event_type, categories, document_version, created_at, ip_address FROM consent_events WHERE user_id = %s ORDER BY created_at DESC """, (user_id,)) def get_current_consent(self, user_id: int) -> dict: """Актуальное согласие пользователя""" latest = self.db.query_one(""" SELECT categories FROM consent_events WHERE user_id = %s AND event_type IN ('granted', 'updated') ORDER BY created_at DESC LIMIT 1 """, (user_id,)) return latest['categories'] if latest else {} , אינדקסים, ארכיטקטורת API |
תיעוד סכמה |
| יישום | כתיבת @app.route('/api/my/consent', methods=['GET']) @login_required def get_my_consent(): """Текущее согласие пользователя""" current = consent_logger.get_current_consent(current_user.id) history = consent_logger.get_user_consent_history(current_user.id) return jsonify({ 'current': current, 'history': [{ 'event': r['event_type'], 'categories': r['categories'], 'version': r['document_version'], 'date': r['created_at'].isoformat(), } for r in history[:10]] }) @app.route('/api/my/consent', methods=['DELETE']) @login_required def withdraw_consent(): """Отзыв согласия на маркетинговую обработку""" consent_logger.log( request, categories={'analytics': False, 'marketing': False, 'preferences': False}, event_type='withdrawn', user_id=current_user.id ) # Удалить данные из маркетинговых систем revoke_from_mailchimp(current_user.email) revoke_from_facebook_custom_audience(current_user.email) return jsonify({'status': 'withdrawn'}) , נקודות קצה REST |
קוד עובד |
| בדיקות | בדיקת אי-שינוי, בדיקות עומס | 100% מעבר בדיקות |
| פריסה | פריסה, ניטור | רץ בסביבת ייצור |
טעויות אופייניות ביישום יומן הסכמות
- שמירת הסכמה נוכחית בלבד, ללא היסטוריית שינויים—אינו עומד בדרישות GDPR.
- שימוש ב-UPDATE במקום INSERT—היומן הופך לניתן לכתיבה מחדש.
- חוסר ב-anonymous_id למשתמשים לא מורשים—אי אפשר לשייך אירועים.
- נתוני הקשר חסרים (IP, user-agent, שפה)—הרגולטור עשוי לדרוש אותם.
- ייצוא DSAR לא מוגדר—יש לכתוב אותו תוך כדי תנועה בעת בקשה.
אנו מבטיחים שיישום יומן ההסכמות שלך יעמוד ברוחה ובאותה של GDPR. צור קשר להערכה חינמית של הפרויקט שלך. הזמן את היישום—קבל פתרון מוכן תוך מספר ימים.







