diff --git a/docs/docs/rights_engine.md b/docs/docs/rights_engine.md
index 23b1807005f529205a8671147f17ad0ecc32a19a..4952e5b62f3cf4f0069df25551f76b87c5442435 100644
--- a/docs/docs/rights_engine.md
+++ b/docs/docs/rights_engine.md
@@ -86,3 +86,29 @@ Response (HTTP 200)
 ```js
 {"message": "xAPI statements successfully stored in LRS", "data": ["6411d628e31a30251d6391f0"]}
 ```
+
+### Anonymized xAPI proxy endpoint
+
+In addition to the aforementioned endpoint for regular xAPI statements, the rights engine offers an endpoint to submit anonymized statements which are not directly associated with a user.
+This works by supplying a "TAN", an arbitrary string sequence, instead of the usual user account information.
+Providers have to keep a list / matching of TANs and users, since the rights engine has no way of knowing which user a given TAN belongs to.
+
+The endpoint is available at `[BASE_URL]/xapi/statements` and accepts valid xAPI statements that contain a TAN in the user object. 
+
+#### Example
+
+```console
+$ curl -i -X POST http://localhost:9001/xapi/tanStatements --data '{ "actor": {"objectType": "Agent", "tan": "u4gueb983fnklerg"}, "timestamp": "2022-03-18T18:07:32+01:00", "context": { "platform": "Moodle", "contextActivities": { "category": { "objectType": "Activity", "definition": { "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/lms", "name": { "de": "moosltest" } }, "id": "https://moosltest.it-services.ruhr-uni-bochum.de" }, "grouping": [ { "objectType": "Activity", "definition": { "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", "name": { "de": "Structured Query Language" } }, "id": "https://moosltest.it-services.ruhr-uni-bochum.de/course/view.php?id=2" }, { "objectType": "Activity", "id": "https://moosltest.it-services.ruhr-uni-bochum.de/mod/quiz/view.php?id=2", "definition": { "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/quiz", "name": { "de": "SQL-Test" } } } ] }, "extensions": { "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course/extensions/roles": "student", "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/quiz_attempt/extensions/timestart": 1647610537, "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/quiz_attempt/extensions/timefinish": 1647623252 } }, "verb": { "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/accessed" }, "object": { "objectType": "Activity", "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", "definition": { "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/quiz_attempt", "name": { "de": "SQL-Test" } } }, "result": { "score": { "raw": 0 } } }' -H "Content-Type: application/json" -H "Authorization: Basic ff9b69722ad9fcf3c77cf7a54b31da6c651f546101ded46f03e53a8f008a66de"
+```
+
+Error response when the endpoint is used without a TAN (HTTP 400)
+
+```js
+{"message": "xAPI statements couldn't be stored in LRS", "data": [{"valid": false, "reason": "TAN missing in statement"}]
+```
+
+The other responses are analogous to the non-anonymized endpoint.
+
+Users can associate anonymized data to their account by using the merge tool included in the frontend.
+For data to be associated, a user has to give a correct TAN and provider.
+In order to prevent abuse by brute-forcing TANs, this tool is rate-limited per user.
\ No newline at end of file
diff --git a/src/.gitignore b/src/.gitignore
index a3c3f3e3fa6a8a24017c2fccd3a4738149a3ee09..d5bdae71688381def56f44406c6e46acbb985444 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -12,4 +12,5 @@ dump.rdb
 backend/jwtRS256.key
 backend/jwtRS256.key.pub
 
-static/upload/analytic-tokens-images/*.*
\ No newline at end of file
+static/upload/analytic-tokens-images/*.*
+!static/upload/analytic-tokens-images/.gitkeep
\ No newline at end of file
diff --git a/src/backend/settings.py b/src/backend/settings.py
index ba9191342bdcb3db2371a49c76d6777802b78c5d..edb77243fab6c56265d2d0a14744fea9dd38ac3a 100644
--- a/src/backend/settings.py
+++ b/src/backend/settings.py
@@ -34,10 +34,13 @@ mimetypes.add_type("text/javascript", ".js", True)
 SECRET_KEY = "django-insecure-4%wn*pt3vpdgk#==(qbtocdh#-2wr58py(ri=(0a^15&&n*gl9"
 
 # SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = env("DEBUG")
+DEBUG = env("DEBUG", default=False) == "True"
+DEBUG_PROPAGATE_EXCEPTIONS = env("DEBUG_PROPAGATE_EXCEPTIONS", default=False) == "True"
 
 TESTING = env("TESTING", default="False")
 
+ANALYTICS_RESULTS_RETENTION = env("ANALYTICS_RESULTS_RETENTION", default=31)
+
 ALLOWED_HOSTS = ["*"]
 
 IDP_ENABLED = env("IDP_ENABLED")
diff --git a/src/backend/urls.py b/src/backend/urls.py
index 056685de1d97c317ce142bb649ed997769ec9b36..996168754d3719686880c38f7abf91b8f42941c8 100644
--- a/src/backend/urls.py
+++ b/src/backend/urls.py
@@ -2,10 +2,13 @@ from django.conf import settings
 from django.contrib import admin
 from django.shortcuts import redirect, render
 from django.urls import include, path, re_path
+from django.views.static import serve  
 from drf_yasg import openapi
 from drf_yasg.views import get_schema_view
 from rest_framework import permissions
 
+import os
+
 from ssoauth import views
 
 schema_view = get_schema_view(
@@ -31,6 +34,7 @@ def render_angular_de(request):
 def render_angular_en(request):
     return render(request, "en/index.html")
 
+
 urlpatterns = [
     path("admin/", admin.site.urls),
     path("api/v1/consents/", include("consents.urls")),
@@ -49,8 +53,9 @@ urlpatterns = [
     re_path(r"^saml2/meta(?:data)?/?$", views.MetadataView.as_view(), name="sso-saml2-meta"),
     re_path(r"^sso-dev/?$", views.DevView.as_view(), name="sso-dev"),
 
-    re_path("app/de/", render_angular_de),
-    re_path("app/en/", render_angular_en),
+    path("app/de/", render_angular_de),
+    path("app/en/", render_angular_en),
+    re_path(r'^app/de/(?P<path>.*)$', serve,{'document_root': os.path.join(settings.BASE_DIR,  "frontend/dist/frontend/de")}),
     re_path(r"^$", detect_lang_redirect),
     re_path(r"^(?:.*)/?$", detect_lang_redirect),
 ]
diff --git a/src/data_disclosure/tasks.py b/src/data_disclosure/tasks.py
index cbedfe2263c0e50ce16b449398e497cad70f96ae..558d48a4844ed7b6e6b5795f82edd480b419b36c 100644
--- a/src/data_disclosure/tasks.py
+++ b/src/data_disclosure/tasks.py
@@ -42,7 +42,8 @@ class DataDisclosureProcessor:
         Get all XAPI statements matching a user email address from database.
         """
         collection = lrs_db["statements"]
-        query = {"actor.mbox": f"mailto:{user_email}"}
+        query = { "$or" : [ {"actor.account.name": f"{user_email}"}, { "actor.mbox": f"mailto:{user_email}" } ] }
+        """ query = {"actor.account.name": f"{user_email}"} """
         cursor = collection.find(query)
         return [
             (
diff --git a/src/frontend/src/app/consent-management/consentDeclaration.ts b/src/frontend/src/app/consent-management/consentDeclaration.ts
index c7672416fb9f0f0647ec03bfa46e479654e6751d..369df5c1da8128a0eb1d3184c8a491bdad991367 100644
--- a/src/frontend/src/app/consent-management/consentDeclaration.ts
+++ b/src/frontend/src/app/consent-management/consentDeclaration.ts
@@ -111,10 +111,10 @@ export const providerSchemaToUserConsent = (providerSchema: ProviderSchema): Use
             verbs: group.verbs.map((verb) => ({
                 ...verb,
                 consented: verb.defaultConsent,
-                objects: verb.objects.map((object) => ({
+                objects: verb.objects ? verb.objects.map((object) => ({
                     ...object,
                     consented: object.defaultConsent
-                }))
+                })) : []
             }))
         })),
         essential_verbs: providerSchema.essential_verbs
diff --git a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts
index 35f2f6471d9a1b135d44fff2a49e124b5b652542..850222f2635a3a9e32c1d97482690fffa85bd121 100644
--- a/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts
+++ b/src/frontend/src/app/consent-management/provider-settings/provider-setting.component.ts
@@ -107,7 +107,7 @@ export class PrivacySettingComponent implements OnInit {
                     }
                 })
         })
-        if (isConfirmationRequired) {
+        if (isConfirmationRequired && this.previousUserConsent != null) {
             const onConfirm = () => this.change.emit(this.consentDeclaration)
             const onCancel = () => (event.source.checked = false)
             this.openVerbWarningDialog(onConfirm, onCancel, verbId)
@@ -161,7 +161,7 @@ export class PrivacySettingComponent implements OnInit {
                     }
                 })
         })
-        if (isConfirmationRequired) {
+        if (isConfirmationRequired && this.previousUserConsent != null) {
             const onConfirm = () => this.change.emit(this.consentDeclaration)
             const onCancel = () => (event.source.checked = false)
             this.openObjectWarningDialog(onConfirm, onCancel,verbId)
diff --git a/src/frontend/src/app/consent-management/wizard/wizard.component.ts b/src/frontend/src/app/consent-management/wizard/wizard.component.ts
index 28767a28f9f9dd781297ab84c502d43ec6da1713..4a434e7b02264d050f72a9ec8ea33d33e6dfec95 100644
--- a/src/frontend/src/app/consent-management/wizard/wizard.component.ts
+++ b/src/frontend/src/app/consent-management/wizard/wizard.component.ts
@@ -18,12 +18,12 @@ export const extractVerbs = (providerSchema: UserConsent, providerId: ProviderId
             provider: providerId,
             id,
             consented: consented,
-            objects: JSON.stringify(
+            objects: objects ? JSON.stringify(
                 objects.map((object) => ({
                     ...object,
                     consented: object.consented
                 }))
-            )
+            ) : []
         }))
     })
 }
diff --git a/src/frontend/src/app/merge-data/merge-data.component.html b/src/frontend/src/app/merge-data/merge-data.component.html
index 9c7fa9ae56b839e8045fca15c1af09054e25f553..c1ed11ae748d8f2bacb96fce242265719fa39e7d 100644
--- a/src/frontend/src/app/merge-data/merge-data.component.html
+++ b/src/frontend/src/app/merge-data/merge-data.component.html
@@ -2,25 +2,27 @@
   <mat-card style='margin: 8px;'>
     <mat-card-title i18n="Merge Data | Header entry @@mergeDataHeader">
           Merge Data
-      </mat-card-title>
-      <mat-card-content>
-
-        <form [formGroup]="mergeForm" (ngSubmit)="submit()">
-          <mat-form-field>
-            <mat-label>Provider</mat-label>
-            <mat-select formControlName="provider">
-              <mat-option *ngFor="let provider of providers" [value]="provider.id">{{ provider.name }}</mat-option>
-            </mat-select>
-          </mat-form-field>
-          <br>
-          <mat-form-field>
-            <mat-label>TAN</mat-label>
-            <input matInput formControlName="tan">
-          </mat-form-field>
-          <br>
-          <button type="submit" mat-raised-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button>
-        </form>
-      </mat-card-content>
+    </mat-card-title>
+    <mat-card-subtitle i18n="@@mergeDataDescription">
+      This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned.
+    </mat-card-subtitle>
+    <mat-card-content>
+      <form [formGroup]="mergeForm" (ngSubmit)="submit()">
+        <mat-form-field>
+          <mat-label>Provider</mat-label>
+          <mat-select formControlName="provider">
+            <mat-option *ngFor="let provider of providers" [value]="provider.id">{{ provider.name }}</mat-option>
+          </mat-select>
+        </mat-form-field>
+        <br>
+        <mat-form-field>
+          <mat-label>TAN</mat-label>
+          <input matInput formControlName="tan">
+        </mat-form-field>
+        <br>
+        <button type="submit" mat-raised-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button>
+      </form>
+    </mat-card-content>
   </mat-card>
 
 </div>
diff --git a/src/frontend/src/locale/messages.de.xlf b/src/frontend/src/locale/messages.de.xlf
index e2d8dc375a4febaef3b47139c27b80ea66677ac6..0f8c52018b81756db7b52a8e650774927a9a4777 100644
--- a/src/frontend/src/locale/messages.de.xlf
+++ b/src/frontend/src/locale/messages.de.xlf
@@ -274,6 +274,15 @@
         <note priority="1" from="description"> Header entry </note>
         <note priority="1" from="meaning">Merge Data </note>
       </trans-unit>
+      <trans-unit id="mergeDataDescription" datatype="html">
+        <source>This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned.</source>
+        <target>Dieses Werkzeug ermöglicht es, Daten, welche mittels einer TAN anonymisiert sind, zum aktuellen Nutzer zuzuordnen. Alle Einträge, welche auf die angegebene TAN und den angegebenen Provider zutreffen, werden zugeordnet.</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/merge-data/merge-data.component.html</context>
+          <context context-type="linenumber">7</context>
+        </context-group>
+      </trans-unit>
+
       <trans-unit id="profileProfileDropDown" datatype="html">
         <source>Profile</source>
         <target>Profil</target>
diff --git a/src/frontend/src/locale/messages.xlf b/src/frontend/src/locale/messages.xlf
index 197fba2c6314644d2cd41f3282ad13ad1a7e4c23..f70d4948c3e0b8c1805050c7b506fd8fd58b7d64 100644
--- a/src/frontend/src/locale/messages.xlf
+++ b/src/frontend/src/locale/messages.xlf
@@ -1124,6 +1124,13 @@
         <note priority="1" from="description"> Header entry </note>
         <note priority="1" from="meaning">Merge Data </note>
       </trans-unit>
+      <trans-unit id="mergeDataDescription" datatype="html">
+        <source>This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/merge-data/merge-data.component.html</context>
+          <context context-type="linenumber">7</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="applicationTokens" datatype="html">
         <source>Application Tokens</source>
         <context-group purpose="location">
diff --git a/src/providers/views.py b/src/providers/views.py
index b4f6948d15f032c3924514831a95a56d98a570f0..3c2cee522b5a5e1d86a0bacdaa7d7c60340e2e29 100644
--- a/src/providers/views.py
+++ b/src/providers/views.py
@@ -464,7 +464,7 @@ class GetProviderData(APIView):
                             {"$or": [
                                 {"actor.mbox": {"$exists": False}},
                                 {"$and": [
-                                    {"actor.mbox": {"$exists": False}},
+                                    {"actor.mbox": {"$exists": True}},
                                     {"actor.mbox": {"$regex": "^mailto"}},
                                 ]},
                                 # also query for system statements added by relevant providers
@@ -490,6 +490,7 @@ class GetProviderData(APIView):
                     ]
                 }
             )
+            print(query)
             cursor = collection.find(query).limit(page_size)
             data = {
                 "verbs": list(set(active_verbs)),
@@ -794,11 +795,12 @@ class RunResultsRetention(APIView):
             AnalyticsToken.objects.get(key=token)
             collection = lrs_db["results"]
             query = {"analytics_token": token}
-            cursor = collection.find(query, sort=[("created_at", -1)]).skip(31)
-            document_ids = [document["_id"] for document in cursor]
-
-            query = {"_id": {"$in": document_ids}}
-            cursor = collection.delete_many(query)
+            for context_id_distinct in collection.distinct("context_id",query):
+                cursor = collection.find({"analytics_token": token, context_id: context_id_distinct}, sort=[("created_at", -1)]).skip(settings.ANALYTICS_RESULTS_RETENTION)
+                document_ids = [document["_id"] for document in cursor]
+                
+                query = {"_id": {"$in": document_ids}}
+                cursor = collection.delete_many(query)
 
             return Response(
                 {"deleted_documents": cursor.deleted_count}, status=status.HTTP_200_OK
diff --git a/src/requirements.txt b/src/requirements.txt
index 8d3937d60d99cbbf943582a65c606add4975b435..cd6f13607c3a6b9d92c2fdf137a0bdc63ac60849 100644
--- a/src/requirements.txt
+++ b/src/requirements.txt
@@ -79,7 +79,7 @@ python-crontab==2.6.0
 python-dateutil==2.8.2
 python3-saml==1.15.0
 pytz==2022.1
-PyYAML==5.4.1
+PyYAML==6.0.1
 pyyaml_env_tag==0.1
 ranger-fm==1.9.3
 redis==4.3.4
diff --git a/src/static/upload/analytic-tokens-images/.gitkeep b/src/static/upload/analytic-tokens-images/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/static/xapi_statement.schema.json b/src/static/xapi_statement.schema.json
index e2a369185b5bb6b568faf3dee1d530effdf5dfcc..72577fd22d2ea8f16a00719665556d252c2357b5 100644
--- a/src/static/xapi_statement.schema.json
+++ b/src/static/xapi_statement.schema.json
@@ -36,7 +36,7 @@
   },
   "required": ["actor", "verb", "object"],
   "$defs": {
-    "iri": { "type": "string", "pattern": "^[a-zA-Z0-9_]+:" },
+    "iri": { "type": "string", "pattern": "^[a-zA-Z0-9_]+" },
     "languageMap": { "type": "object" },
     "agent": {
       "type": "object",
@@ -111,7 +111,7 @@
       },
       "required": ["id"]
     },
-    "mbox": { "type": "string", "pattern": "mailto:[a-z0-9._%+!$&*=^|~#%{}/-]+@([a-z0-9-]+.){1,}([a-z]{2,22})" },
+    "mbox": { "type": "string", "pattern": "(mailto:[a-z0-9._%+!$&*=^|~#%{}/-]+@([a-z0-9-]+.){1,}([a-z]{2,22}))|(system:[0-9]+)" },
     "mbox_sha1sum": { "type": "string", "pattern": "^\b[0-9a-f]{5,40}$" },
     "account": {
       "type": "object",
diff --git a/src/xapi/tests/test_xapi_statement_validation.py b/src/xapi/tests/test_xapi_statement_validation.py
index ad8c8f265f615eb947e6b857d558d01e75073a0e..a1079271ac153d999eb8e6e9b434c4dbf53d9f8f 100644
--- a/src/xapi/tests/test_xapi_statement_validation.py
+++ b/src/xapi/tests/test_xapi_statement_validation.py
@@ -7,7 +7,7 @@ from jsonschema import ValidationError, validate
 PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
 
 
-class TestxAPIStatementeValidation(TestCase):
+class TestxAPIStatementValidation(TestCase):
     def setUp(self):
         with open(os.path.join(PROJECT_PATH, "static/xapi_statement.schema.json")) as f:
             self.schema = json.load(f)
@@ -167,3 +167,24 @@ class TestxAPIStatementeValidation(TestCase):
             validate(statement, self.schema)
         except ValueError:
             assert False
+
+    def test_validate_statement_with_tan(self):
+        statement = {
+            "actor": {
+                "objectType": "Agent",
+                "tan": "u4gueb983fnklerg",
+            },
+            "verb": {
+                "id": "http://adlnet.gov/expapi/verbs/failed",
+                "display": {"en-US": "failed"},
+            },
+            "object": {
+                "id": "https://example.adlnet.gov/AUidentifier",
+                "objectType": "Activity",
+            },
+        }
+
+        try:
+            validate(statement, self.schema)
+        except ValueError:
+            assert False
\ No newline at end of file
diff --git a/src/xapi/tests/tests.py b/src/xapi/tests/tests.py
index 4467c61f5627b96c6fad2bd447ec25ee79594841..5e764908923f95d9cd69d0993fa4a5c1325ccc93 100644
--- a/src/xapi/tests/tests.py
+++ b/src/xapi/tests/tests.py
@@ -197,9 +197,13 @@ class XAPITestCase(TestCase):
                 "actor": {"mbox": f"system:{provider.id}"},
                 "verb": {"id": "some_id"},
                 "object": {
-                    "id": "some_other_id",
+                    "id": "someOtherId",
                     "objectType": "Activity",
-                    "definition": "object_definition",
+                    "definition": {
+                        "name": {
+                            "de-DE": "Testobjekt"
+                        }
+                    },
                 },
                 "timestamp": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
             },
@@ -506,7 +510,7 @@ class TestxAPIStatementActorAccount(BaseTestCase):
                     {
                         "valid": False,
                         "accepted": False,
-                        "reason": "User missing in statement",
+                        "reason": "'name' is a required property",
                     }
                 ],
             },
diff --git a/src/xapi/views.py b/src/xapi/views.py
index 4af3b782062a9b77b3e71a87599925342ff3a361..31a7f138295c3d236e5f7bf67e485678102994d2 100644
--- a/src/xapi/views.py
+++ b/src/xapi/views.py
@@ -52,7 +52,7 @@ def process_statement(x_api_statement, provider, latest_schema):
     Process xAPI statement by checking for validation errors and user consent settings.
     """
     try:
-        validate(x_api_statement, x_api_statement)
+        validate(x_api_statement, schema)
     except ValidationError as e:
         return {"valid": False, "accepted": False, "reason": e.message}
 
@@ -164,7 +164,7 @@ def process_tan_statement(x_api_statement):
     Process xAPI statement by checking for validation errors.
     """
     try:
-        validate(x_api_statement, x_api_statement)
+        validate(x_api_statement, schema)
     except ValidationError as e:
         return {"valid": False, "reason": e.message}