[{"data":1,"prerenderedAt":1723},["ShallowReactive",2],{"search-de":3,"content-de-developer\u002Fcampaign-internals":4,"surround-de-\u002Fdeveloper\u002Fcampaign-internals":1716},[],{"id":5,"title":6,"body":7,"description":1708,"extension":1709,"meta":1710,"navigation":1711,"path":1712,"seo":1713,"stem":1714,"__hash__":1715},"content_de\u002F3.developer\u002F16.campaign-internals.md","Kampagnen-Interna",{"type":8,"value":9,"toc":1680},"minimark",[10,35,59,64,107,112,197,201,216,306,321,367,371,386,474,512,533,539,555,582,623,627,653,745,748,806,827,831,848,858,966,981,985,1016,1237,1241,1290,1309,1313,1333,1436,1462,1531,1544,1548,1555,1605,1627,1662,1666,1670,1675],[11,12,13,14,18,19,23,24,29,30,34],"p",{},"Diese Seite ist die Entwicklerreferenz für den Kampagnen-Versandpfad im Convex-Backend — was zwischen dem Klick auf ",[15,16,17],"strong",{},"Senden"," und dem Postfach einer empfangenden Person passiert. Sie behandelt die beiden Lebenszyklus-Zustandsmaschinen, das Versand-Pre-Flight-Gate, die Orchestrator-Pipeline, die ",[20,21,22],"code",{},"emailSends","-Datensätze pro Empfänger und die ratenbegrenzten Workpools. Die anwenderseitige Anleitung finden Sie unter ",[25,26,28],"a",{"href":27},"\u002Fguide\u002Fsend-campaign","Kampagne senden & überwachen","; wie die Zielgruppe in Empfänger aufgelöst wird, steht unter ",[25,31,33],{"href":32},"\u002Fdeveloper\u002Faudience-internals","Zielgruppen-Interna",".",[36,37,40],"callout",{"title":38,"type":39},"Zwei-Maschinen-Design","info",[11,41,42,43,46,47,50,51,54,55,58],{},"Eine Kampagnenzeile trägt ",[15,44,45],{},"zwei unabhängige Statusspalten"," — ",[20,48,49],{},"status"," (den Kampagnen-Lebenszyklus) und ",[20,52,53],{},"abTestStatus"," (den A\u002FB-Test-Lebenszyklus). Jede hat ihren eigenen Graphen erlaubter Kanten und ihr eigenes Single-Writer-Modul. Diese Aufteilung ist in ",[20,56,57],{},"docs\u002Fadr\u002F0017-campaign-lifecycle-modules.md"," festgehalten.",[60,61,63],"h2",{"id":62},"zustandsmaschine-des-kampagnen-lebenszyklus","Zustandsmaschine des Kampagnen-Lebenszyklus",[11,65,66,67,69,70,73,74,77,78,81,82,85,86,89,90,89,93,89,96,99,100,103,104,106],{},"Das Feld ",[20,68,49],{}," der Kampagne wird von genau einem Modul geschrieben — ",[20,71,72],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002Flifecycle.ts",". Dessen öffentliche ",[20,75,76],{},"transition","-Mutation ist der ",[15,79,80],{},"einzige"," Schreiber von ",[20,83,84],{},"campaigns.status"," und der Begleitfelder, die sie zusammen damit patcht (",[20,87,88],{},"sentAt",", ",[20,91,92],{},"cancelledAt",[20,94,95],{},"scheduledAt",[20,97,98],{},"contentBlockReason"," sowie der Stats-Null-Block beim Versand). Ein direktes ",[20,101,102],{},"ctx.db.patch"," von ",[20,105,49],{}," an anderer Stelle ist ein Schichtverstoß.",[108,109,111],"h3",{"id":110},"die-sechs-status","Die sechs Status",[113,114,115,128],"table",{},[116,117,118],"thead",{},[119,120,121,125],"tr",{},[122,123,124],"th",{},"Status",[122,126,127],{},"Bedeutung",[129,130,131,142,152,162,172,182],"tbody",{},[119,132,133,139],{},[134,135,136],"td",{},[20,137,138],{},"draft",[134,140,141],{},"Bearbeitbar; nicht eingereiht. Der Standard für eine neue Kampagne.",[119,143,144,149],{},[134,145,146],{},[20,147,148],{},"scheduled",[134,150,151],{},"Ein künftiger Versandzeitpunkt ist gesetzt; der Orchestrator ist für dann eingeplant.",[119,153,154,159],{},[134,155,156],{},[20,157,158],{},"sending",[134,160,161],{},"Der Orchestrator läuft (oder seine Sends sind unterwegs).",[119,163,164,169],{},[134,165,166],{},[20,167,168],{},"sent",[134,170,171],{},"Terminal. Jeder eingereihte Send hat die Queue verlassen.",[119,173,174,179],{},[134,175,176],{},[20,177,178],{},"cancelled",[134,180,181],{},"Terminal. Die Nutzerin hat vor oder während des Versands abgebrochen.",[119,183,184,189],{},[134,185,186],{},[20,187,188],{},"pending_review",[134,190,191,192,196],{},"Der Content-Scanner hat den Versand als ",[193,194,195],"em",{},"verdächtig"," markiert; er wird bis zur Prüfung zurückgehalten.",[108,198,200],{"id":199},"erlaubte-kanten","Erlaubte Kanten",[11,202,203,204,207,208,211,212,215],{},"Der Reducer lehnt jeden Übergang ab, der nicht in ",[20,205,206],{},"LEGAL_EDGES"," steht. Es wird keine Exception geworfen — ein unzulässiger Aufruf gibt ",[20,209,210],{},"{ ok: false, reason: 'illegal_edge' }"," zurück, und ein Übergang aus einem terminalen Status gibt ",[20,213,214],{},"reason: 'terminal'"," zurück. Die Aufrufer übersetzen das Ergebnis in eine Antwort an die Nutzerin.",[113,217,218,231],{},[116,219,220],{},[119,221,222,225],{},[122,223,224],{},"Von",[122,226,227,228],{},"Erlaubte ",[20,229,230],{},"to",[129,232,233,245,259,273,285,296],{},[119,234,235,239],{},[134,236,237],{},[20,238,138],{},[134,240,241,89,243],{},[20,242,148],{},[20,244,158],{},[119,246,247,251],{},[134,248,249],{},[20,250,148],{},[134,252,253,89,255,89,257],{},[20,254,138],{},[20,256,178],{},[20,258,158],{},[119,260,261,265],{},[134,262,263],{},[20,264,158],{},[134,266,267,89,269,89,271],{},[20,268,168],{},[20,270,138],{},[20,272,188],{},[119,274,275,279],{},[134,276,277],{},[20,278,188],{},[134,280,281,89,283],{},[20,282,158],{},[20,284,138],{},[119,286,287,291],{},[134,288,289],{},[20,290,168],{},[134,292,293],{},[193,294,295],{},"(terminal)",[119,297,298,302],{},[134,299,300],{},[20,301,178],{},[134,303,304],{},[193,305,295],{},[11,307,308,309,312,313,316,317,320],{},"Eine Selbstschleife (",[20,310,311],{},"from === to",") ist ",[15,314,315],{},"idempotent",": Sie schreibt eine Audit-Log-Zeile, gibt ",[20,318,319],{},"applied: 'recorded'"," zurück und erzeugt kein Patch, keinen Scheduler-Hop und kein PostHog-Event. Genau das macht einen erneut ausgelösten Scheduler-Tick unbedenklich.",[36,322,325],{"title":323,"type":324},"Freigeben\u002FAblehnen der Prüfung ist noch nicht angebunden","warning",[11,326,327,328,331,332,335,336,338,339,342,343,346,347,350,351,354,355,358,359,362,363,366],{},"Der Content-Scanner kann eine ",[193,329,330],{},"verdächtige"," Kampagne ",[15,333,334],{},"in"," ",[20,337,188],{}," bewegen, aber die beiden Kanten ",[15,340,341],{},"heraus"," (",[20,344,345],{},"pending_review → sending"," freigeben, ",[20,348,349],{},"pending_review → draft"," ablehnen) haben heute ",[15,352,353],{},"keinen Produktaufrufer"," — es gibt in diesem OSS-Repository keine Review-Queue-UI und keine Freigabe-\u002FAblehn-Mutation. Die Kanten existieren im Graphen erlaubter Kanten und die Audit-Aktionen (",[20,356,357],{},"campaign.review_approved"," \u002F ",[20,360,361],{},"campaign.review_rejected",") sind definiert, aber eine Betreiberin müsste die interne Mutation ",[20,364,365],{},"lifecycle.transition"," von Hand aufrufen, um eine zurückgehaltene Kampagne freizugeben.",[108,368,370],{"id":369},"effekte","Effekte",[11,372,373,374,377,378,381,382,385],{},"Der Reducer ist pur: Aus der geladenen Kampagne, der typisierten Eingabe und einer ",[20,375,376],{},"userId"," gibt er ein ",[20,379,380],{},"patch"," plus eine Liste von ",[20,383,384],{},"effects"," zurück. Ein separater Runner wendet sie der Reihe nach an, atomar mit dem Patch der Zeile. Es gibt vier Effektarten:",[113,387,388,398],{},[116,389,390],{},[119,391,392,395],{},[122,393,394],{},"Effekt",[122,396,397],{},"Wann er auslöst",[129,399,400,410,435,453],{},[119,401,402,407],{},[134,403,404],{},[20,405,406],{},"audit_log",[134,408,409],{},"Bei jedem Übergang (auch bei idempotenten Selbstschleifen).",[119,411,412,417],{},[134,413,414],{},[20,415,416],{},"schedule_campaign_send_orchestrator",[134,418,419,420,423,424,427,428,431,432,34],{},"Bei ",[20,421,422],{},"→ scheduled"," (Verzögerung = ",[20,425,426],{},"scheduledAt - at",") und ",[20,429,430],{},"→ sending"," (Verzögerung = 0). Konsument ist ",[20,433,434],{},"campaigns.send.startCampaignSend",[119,436,437,442],{},[134,438,439],{},[20,440,441],{},"track_event",[134,443,444,445,358,447,358,449,452],{},"Nur bei nutzergetriebenem ",[20,446,422],{},[20,448,430],{},[20,450,451],{},"→ cancelled",". Wird an PostHog gemeldet.",[119,454,455,460],{},[134,456,457],{},[20,458,459],{},"start_ab_test_if_enabled",[134,461,462,463,465,466,469,470,473],{},"Maschinenübergreifender Anstoß bei ",[20,464,430],{},", wenn ",[20,467,468],{},"isABTest",". Ruft ",[20,471,472],{},"→ testing"," des A\u002FB-Test-Lebenszyklus auf.",[11,475,476,477,479,480,483,484,487,488,89,491,89,494,89,497,500,501,503,504,507,508,511],{},"Das Argument ",[20,478,376],{}," unterscheidet nutzergetriebene von systemseitigen Übergängen. Nutzerseitige Mutationen übergeben ",[20,481,482],{},"session.userId","; interne Aufrufer übergeben ein Tag ",[20,485,486],{},"'system:\u003Csource>'"," (z. B. ",[20,489,490],{},"'system:scheduler_tick'",[20,492,493],{},"'system:content_scan'",[20,495,496],{},"'system:orchestrator'",[20,498,499],{},"'system:send_completion'","). Das Audit-Log hält das Tag wortgetreu fest, und der ",[20,502,441],{},"-Effekt wird für jeden Aufrufer mit ",[20,505,506],{},"system:","-Präfix ",[15,509,510],{},"unterdrückt"," — Hintergrundübergänge verschmutzen PostHog also nie.",[36,513,516],{"title":514,"type":515},"Semantik von „Campaign sent\"","tip",[11,517,518,519,522,523,525,526,529,530,532],{},"Das PostHog-Event ",[20,520,521],{},"campaign_sent"," löst auf der Kante ",[20,524,430],{}," aus, nicht bei ",[20,527,528],{},"→ sent",". Für die Nutzerin bedeutet „die Kampagne wurde gesendet\" so viel wie „der Versand wurde angestoßen\". Die Kante ",[20,531,345],{}," ist vom Event ausgenommen — sie wäre eine Freigabe nach Prüfung, kein nutzerinitiierter Versand (und hat, wie oben erwähnt, ohnehin noch keinen Aufrufer).",[108,534,536,538],{"id":535},"sent-erreichen-batch-abschluss",[20,537,168],{}," erreichen: Batch-Abschluss",[11,540,541,543,544,546,547,550,551,554],{},[20,542,528],{}," wird nicht von der Nutzerin ausgelöst. Jeder Workpool-Callback pro Send bewegt seine eigene ",[20,545,22],{},"-Zeile weiter, aber die Kampagne selbst schließt erst ab, wenn ihr ",[15,548,549],{},"letzter eingereihter Send die Queue verlässt",". ",[20,552,553],{},"tryCompleteCampaign"," (im selben Modul) ist der gemeinsame Wächter, der von zwei Stellen aufgerufen wird:",[556,557,558,565],"ul",{},[559,560,561,564],"li",{},[20,562,563],{},"reconcileCampaignCompletion"," — der Einstiegspunkt pro Send, aufgerufen aus dem Workpool-Completion-Callback, nachdem jeder Send einen terminalen Status erreicht hat. Ein günstiger No-Op, bis die Kampagne wirklich fertig ist.",[559,566,567,570,571,574,575,578,579,581],{},[20,568,569],{},"reconcileSendingCampaigns"," — ein Sicherheitsnetz-Cron (",[20,572,573],{},"process scheduled campaigns"," läuft jede Minute; dieser Sweep läuft ebenfalls jede Minute als ",[20,576,577],{},"reconcile sending campaigns","), der jede noch in ",[20,580,158],{}," befindliche Kampagne abgleicht. Er fängt Callbacks ab, die fehlgeschlagen sind, oder finale Sends, die statt vom Workpool durch einen Provider-Webhook überführt wurden.",[11,583,584,586,587,465,589,592,593,595,596,599,600,603,604,607,608,610,611,335,614,616,617,550,620,622],{},[20,585,553],{}," wechselt nur dann zu ",[20,588,168],{},[15,590,591],{},"alles"," davon gilt: Der Status ist ",[20,594,158],{},"; der gecheckpointete Send-Walker hat das Streamen beendet (keine ",[20,597,598],{},"campaignSendJobs","-Zeile mehr in Phase ",[20,601,602],{},"resolving","); ein etwaiger A\u002FB-Test hat ",[20,605,606],{},"winner_selected"," erreicht (sonst würde der Rest-Versand der zweiten Welle übersprungen); mindestens eine ",[20,609,22],{},"-Zeile existiert; und ",[15,612,613],{},"keine",[20,615,22],{},"-Zeile ist noch ",[20,618,619],{},"queued",[20,621,619],{}," ist der einzige nicht terminale Send-Status, sein Fehlen bedeutet also, dass die Kampagne fertig ist.",[60,624,626],{"id":625},"ab-test-lebenszyklus-und-die-rest-versand-garantie","A\u002FB-Test-Lebenszyklus und die Rest-Versand-Garantie",[11,628,629,630,632,633,636,637,639,640,642,643,89,646,89,649,652],{},"Die Spalte ",[20,631,53],{}," hat ihre eigene Maschine in ",[20,634,635],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002FabTestLifecycle.ts"," — dieselbe Zeile, andere Spalte, eigener Graph. Ihre ",[20,638,76],{},"-Mutation ist der einzige Schreiber von ",[20,641,53],{}," und dessen Begleitfeldern (",[20,644,645],{},"abTestConfig",[20,647,648],{},"abWinner",[20,650,651],{},"abWinnerSelectedAt"," sowie der Block zum Zurücksetzen der Variantenstatistiken beim Deaktivieren).",[113,654,655,668],{},[116,656,657],{},[119,658,659,661,665],{},[122,660,224],{},[122,662,227,663],{},[20,664,230],{},[122,666,667],{},"Auslöser",[129,669,670,688,707,727],{},[119,671,672,677,682],{},[134,673,674],{},[193,675,676],{},"(keiner)",[134,678,679],{},[20,680,681],{},"pending",[134,683,684,687],{},[20,685,686],{},"enableABTest","-Mutation",[119,689,690,694,699],{},[134,691,692],{},[20,693,681],{},[134,695,696],{},[20,697,698],{},"testing",[134,700,701,702,704,705],{},"Maschinenübergreifend — der Effekt ",[20,703,459],{}," des Kampagnen-Lebenszyklus bei ",[20,706,430],{},[119,708,709,713,717],{},[134,710,711],{},[20,712,698],{},[134,714,715],{},[20,716,606],{},[134,718,719,722,723,726],{},[20,720,721],{},"declareABTestWinner"," (manuell) oder ",[20,724,725],{},"autoDeclareWinner"," (kriteriengesteuert)",[119,728,729,734,739],{},[134,730,731],{},[20,732,733],{},"*",[134,735,736],{},[20,737,738],{},"none",[134,740,741,744],{},[20,742,743],{},"disableABTest"," (vollständiger Reset)",[11,746,747],{},"Zwei Effekte garantieren, dass die zurückgehaltene Zielgruppe nie verwaist:",[556,749,750,791],{},[559,751,752,757,758,760,761,764,765,768,769,772,773,776,777,780,781,784,785,787,788,790],{},[15,753,754],{},[20,755,756],{},"schedule_auto_winner"," löst bei ",[20,759,472],{}," aus, wenn ",[20,762,763],{},"winnerCriteria"," nicht ",[20,766,767],{},"manual"," ist und ",[20,770,771],{},"testDuration"," (Stunden) gesetzt ist. Es plant ",[20,774,775],{},"campaigns.abTest.autoDeclareWinner"," nach dem Testfenster ein. Ohne diesen Effekt würde eine ",[20,778,779],{},"open_rate","- \u002F ",[20,782,783],{},"click_rate","-Kampagne (der Wizard-Standard) für immer in ",[20,786,698],{}," sitzen und die 40–60 % Restzielgruppe nie versendet. Manuelle Kriterien setzen stattdessen darauf, dass die Nutzerin einen „Gewinner wählen\"-Button klickt, den die Report-Seite nur bei ",[20,789,767],{}," anzeigt.",[559,792,793,757,798,801,802,805],{},[15,794,795],{},[20,796,797],{},"schedule_winner_remainder",[20,799,800],{},"→ winner_selected"," aus. Es plant ",[20,803,804],{},"campaigns.send.sendCampaignWinnerToRemainder"," (den Orchestrator der zweiten Phase, siehe unten) ein, um den Inhalt der Siegervariante an alle Zurückgehaltenen auszuliefern.",[36,807,809],{"title":808,"type":39},"Was splitPercentage bedeutet",[11,810,811,814,815,818,819,822,823,826],{},[20,812,813],{},"splitPercentage"," (validiert auf 10–50) ist der Prozentsatz ",[15,816,817],{},"pro Variante der Testkohorte",". Die Kohorte ist damit ",[20,820,821],{},"2 × splitPercentage %"," der Zielgruppe — ",[20,824,825],{},"20"," erzeugt also eine 40-%-Testkohorte (20 % A, 20 % B) und einen zurückgehaltenen Rest von 60 %.",[60,828,830],{"id":829},"versand-pre-flight-validierung","Versand-Pre-Flight-Validierung",[11,832,833,834,836,837,839,840,843,844,847],{},"Bevor irgendein Aufrufer eine Kampagne nach ",[20,835,148],{}," oder ",[20,838,158],{}," überführt, führt er das Pre-Flight in ",[20,841,842],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002Fpreflight.ts"," aus. Der Lebenszyklus-Reducer ",[15,845,846],{},"vertraut seiner Eingabe"," — er validiert die Versandbereitschaft nicht erneut —, sodass das Pre-Flight das eine Gate ist, das sich die vier Versand-\u002FPlanungs-Einstiegspunkte (und der Orchestrator zum Auslösezeitpunkt) teilen.",[11,849,850,853,854,857],{},[20,851,852],{},"validateReadyToSend"," gibt eine ",[20,855,856],{},"PreflightResult","-Union zurück; die erste fehlschlagende Prüfung gewinnt. Die Prüfungen in ihrer Reihenfolge:",[113,859,860,872],{},[116,861,862],{},[119,863,864,869],{},[122,865,866],{},[20,867,868],{},"reason",[122,870,871],{},"Bedingung",[129,873,874,887,899,911,931,941,951],{},[119,875,876,881],{},[134,877,878],{},[20,879,880],{},"no_template",[134,882,883,886],{},[20,884,885],{},"emailTemplateId"," ist nicht gesetzt",[119,888,889,894],{},[134,890,891],{},[20,892,893],{},"no_audience",[134,895,896,886],{},[20,897,898],{},"audience",[119,900,901,906],{},[134,902,903],{},[20,904,905],{},"no_from_email",[134,907,908,886],{},[20,909,910],{},"fromEmail",[119,912,913,918],{},[134,914,915],{},[20,916,917],{},"sending_not_allowed",[134,919,920,921,924,925,836,928],{},"Der ",[20,922,923],{},"abuseStatus"," der Instanz ist ",[20,926,927],{},"suspended",[20,929,930],{},"banned",[119,932,933,938],{},[134,934,935],{},[20,936,937],{},"no_delivery_provider",[134,939,940],{},"Kein E-Mail-Delivery-Provider (EMAIL_PROVIDER + Credentials oder eine Provider-Route) ist konfiguriert. Ein verbundenes externes IMAP-Postfach erfüllt das nicht.",[119,942,943,948],{},[134,944,945],{},[20,946,947],{},"domain_not_verified",[134,949,950],{},"Die Domain der Absenderadresse ist nicht verifiziert",[119,952,953,958],{},[134,954,955],{},[20,956,957],{},"scheduled_in_past",[134,959,960,962,963,965],{},[20,961,95],{}," liegt in der Vergangenheit (nur geprüft, wenn ",[20,964,95],{}," übergeben wird)",[11,967,968,969,972,973,976,977,980],{},"Die interne Query ",[20,970,971],{},"validateReadyToSendQuery"," kapselt dieselbe Logik, damit der Orchestrator das Pre-Flight ",[15,974,975],{},"zum Auslösezeitpunkt"," erneut ausführen kann — und so Zustände abfängt, die zwischen dem ursprünglichen ",[20,978,979],{},"schedule","-Aufruf und dem Scheduler-Tick abgedriftet sind (Organisation gesperrt, Template gelöscht, Domain-Verifizierung abgelaufen).",[60,982,984],{"id":983},"der-versand-orchestrator","Der Versand-Orchestrator",[11,986,987,342,989,992,993,996,997,1000,1001,1003,1004,1007,1008,1011,1012,1015],{},[20,988,434],{},[20,990,991],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002Fsend.ts",") ist die einzige laufende Action, die eine Kampagne von ",[20,994,995],{},"scheduled | sending"," durch die gesamte Vorbereitungs-Pipeline führt. Sie wird von drei Produzenten ausgelöst: dem Cron-Tick ",[20,998,999],{},"processScheduledCampaigns",", dem Effekt ",[20,1002,416],{}," des Lebenszyklus und einer direkten Neuplanung in ",[20,1005,1006],{},"campaigns\u002Fscheduling.ts",". Sie ist der einzige Schreiber von ",[20,1009,1010],{},"emailSends.abVariant"," und der einzige Aufrufer von ",[20,1013,1014],{},"enqueueCampaignEmails"," in der ersten Phase.",[1017,1018,1019,1023,1052,1056,1065,1071,1084,1088,1139,1143,1166,1170,1176,1180,1223],"steps",{},[108,1020,1022],{"id":1021},"wächter-gegen-status-races","Wächter gegen Status-Races",[11,1024,1025,1026,836,1028,1030,1031,1033,1034,1037,1038,1040,1041,1044,1045,1048,1049,1051],{},"War die Kampagne ",[20,1027,178],{},[20,1029,138],{}," (ungeplant) oder bereits ",[20,1032,168],{},", gibt der Orchestrator ",[20,1035,1036],{},"skipped: true"," mit einem Grund zurück. Bei ",[20,1039,158],{}," überspringt er ",[15,1042,1043],{},"nicht"," — der ",[20,1046,1047],{},"sendNow","-Pfad kommt bereits auf ",[20,1050,158],{}," umgestellt an, und Übergänge in denselben Zustand lösen den Orchestrator-Effekt nicht erneut aus.",[108,1053,1055],{"id":1054},"pre-flight-erneut-ausführen","Pre-Flight erneut ausführen",[11,1057,1058,1060,1061,1064],{},[20,1059,971],{}," läuft erneut. Ein Fehlschlag gibt ",[20,1062,1063],{},"skipped"," mit der Pre-Flight-Meldung zurück — es werden keine Empfänger angefasst.",[108,1066,1068,1069],{"id":1067},"umschalten-auf-sending","Umschalten auf ",[20,1070,158],{},[11,1072,1073,1074,1076,1077,1079,1080,1083],{},"Ist die Kampagne noch ",[20,1075,148],{},", überführt der Orchestrator sie über den Lebenszyklus nach ",[20,1078,158],{}," (userId ",[20,1081,1082],{},"system:scheduler_tick",").",[108,1085,1087],{"id":1086},"content-scan-gate","Content-Scan-Gate",[11,1089,1090,1091,1094,1095,1097,1098,1101,1102,89,1105,1101,1108,1111,1112,1115,1116,1119,1120,1122,1123,1125,1126,1128,1129,1132,1133,1135,1136,1138],{},"Betreff und gerendertes HTML werden auf Spam, Phishing und verbotene Inhalte geprüft. Ist ein ",[20,1092,1093],{},"GOOGLE_SAFE_BROWSING_API_KEY"," konfiguriert, werden Link-URLs zusätzlich gegen Google Safe Browsing geprüft (ein Fehlschlag dieser Prüfung blockiert den Versand ",[15,1096,1043],{},"). Der kombinierte Score klassifiziert den Versand: ",[20,1099,1100],{},"≥ 40"," → ",[20,1103,1104],{},"blocked",[20,1106,1107],{},"≥ 15",[20,1109,1110],{},"suspicious",", sonst ",[20,1113,1114],{},"clean",". Ein nicht sauberes Ergebnis wird in ",[20,1117,1118],{},"contentScanResults"," persistiert. Ein ",[20,1121,1104],{},"-Versand setzt die Kampagne mit einem ",[20,1124,98],{}," auf ",[20,1127,138],{}," zurück (userId ",[20,1130,1131],{},"system:content_scan","); ein ",[20,1134,1110],{},"-Versand überführt sie nach ",[20,1137,188],{},". Beides bricht den Orchestrator ab.",[108,1140,1142],{"id":1141},"archiv-snapshot","Archiv-Snapshot",[11,1144,1145,1146,1149,1150,1153,1154,1157,1158,1161,1162,1165],{},"Ist die Archivierung aktiviert (kampagnenweise über ",[20,1147,1148],{},"archiveEnabled",", ersatzweise über das aufgelöste Feature-Flag ",[20,1151,1152],{},"campaigns.archive",") ",[15,1155,1156],{},"und"," ist ",[20,1159,1160],{},"SITE_URL"," gesetzt, erzeugt der Orchestrator einen öffentlichen HTML-Archiv-Snapshot, speichert ihn mit einem 24-Zeichen-Token und berechnet eine ",[20,1163,1164],{},"viewInBrowserUrl",", die in jeden Send eingefädelt wird.",[108,1167,1169],{"id":1168},"zielgruppe-einfrieren","Zielgruppe einfrieren",[11,1171,1172,1175],{},[20,1173,1174],{},"freezeCampaignAudience"," snapshottet die Filter einer Segment-Zielgruppe zum Versandzeitpunkt (ADR-0033), sodass die Kampagne exakt die Zielgruppe reproduziert, auf die sie abzielte, selbst wenn das Segment später bearbeitet wird. Themen-Zielgruppen und bereits eingefrorene Segmente gehen unverändert durch.",[108,1177,1179],{"id":1178},"send-job-checkpoint-öffnen-und-empfänger-streamen","Send-Job-Checkpoint öffnen und Empfänger streamen",[11,1181,1182,1183,1185,1186,1189,1190,1193,1194,342,1197,1200,1201,1204,1205,1208,1209,1212,1213,1215,1216,1219,1220,1083],{},"Der Orchestrator löst die gesamte Zielgruppe nicht mehr inline auf. Er öffnet einen ",[20,1184,598],{},"-Checkpoint (",[20,1187,1188],{},"createSendJob",", in ",[20,1191,1192],{},"campaigns\u002FsendJob.ts",") und plant ",[20,1195,1196],{},"resolveCampaignPage",[20,1198,1199],{},"campaigns\u002Fsend.ts:611",") ein, einen sich selbst neu einplanenden Walker, der die eingefrorene Zielgruppe über ",[20,1202,1203],{},"resolveRecipientPage"," seitenweise in ",[15,1206,1207],{},"beschränkten Seiten"," streamt. Jede Seite wendet das eine Eignungsprädikat an — lebender Kontakt → E-Mail vorhanden → nicht unterdrückt → Double-Opt-in bestätigt (",[15,1210,1211],{},"nur bei Themen-Zielgruppen"," — Segment-Zielgruppen sind nie DOI-gegatet) — gruppiert dann nach Sprache und ordnet jeden Kontakt dem A\u002FB-Split zu (siehe unten), wobei die Seite eingereiht wird, bevor die nächste geholt wird. Der Walker plant sich selbst neu, bis die Zielgruppe erschöpft ist. Der Schnellpfad „leere Zielgruppe → ",[20,1214,168],{},"\" liegt im Last-Page-Zweig des Walkers (",[20,1217,1218],{},"campaigns\u002Fsend.ts:884-895",", userId ",[20,1221,1222],{},"system:orchestrator",[11,1224,1225,1226,1229,1230,1232,1233,1236],{},"Empfänger werden für i18n nach ihrer Sprachpräferenz gruppiert (ersatzweise nach der ",[20,1227,1228],{},"defaultLanguage"," des Templates). Innerhalb jeder Sprachgruppe ordnet das A\u002FB-Fanout (falls ",[20,1231,468],{}," und ",[20,1234,1235],{},"abTestStatus === 'testing'",") jeden Kontakt per Hash der Testkohorte oder dem zurückgehaltenen Rest zu, danach werden die Varianten A und B getrennt eingereiht. Nicht-A\u002FB-Gruppen reihen alle mit dem Inhalt von Variante A ein, ohne Tag.",[108,1238,1240],{"id":1239},"details-zum-ab-fanout","Details zum A\u002FB-Fanout",[11,1242,1243,342,1246,1249,1250,1252,1253,1255,1256,836,1258,1260,1261,103,1264,342,1267,1270,1271,1274,1275,1278,1279,1282,1283,1286,1287,1289],{},[20,1244,1245],{},"resolveAbFanout",[20,1247,1248],{},"campaigns\u002FsendVariantSplit.ts",") liefert nur dann ein Fanout, wenn für die Kampagne ",[20,1251,468],{}," gilt, ",[20,1254,1235],{}," ist und eine Konfiguration vorliegt — eine Kampagne in ",[20,1257,606],{},[20,1259,681],{}," splittet in der ersten Phase also nie. Der Split ist ein ",[15,1262,1263],{},"deterministischer FNV-1a-Hash",[20,1265,1266],{},"(campaignId:contactId)",[20,1268,1269],{},"hashFraction","), verglichen mit ",[20,1272,1273],{},"testFraction = 2 × splitPercentage \u002F 100",": ",[20,1276,1277],{},"h \u003C testFraction"," ist die Testkohorte (per ",[20,1280,1281],{},"variantForHash"," am Kohorten-Mittelpunkt in A\u002FB unterteilt), und ",[20,1284,1285],{},"h >= testFraction"," ist der zurückgehaltene Rest. Empfänger strömen Seite für Seite durch den gecheckpointeten Send-Walker (",[20,1288,1196],{},") — sie werden nie gemischt oder vollständig materialisiert.",[11,1291,1292,1293,1295,1296,1298,1299,1302,1303,1305,1306,1308],{},"Der zurückgehaltene Rest wird in der ersten Phase nicht versendet. Nachdem ein Gewinner feststeht, löst die Action der zweiten Phase, ",[20,1294,804],{},", die Zielgruppe erneut auf, schließt jeden Kontakt aus, der für diese Kampagne bereits eine ",[20,1297,22],{},"-Zeile hat (das deckt die Testkohorte ab und schützt vor Doppelversand), und reiht den Inhalt der Siegervariante für den Rest ein. Beide Phasen teilen sich den Helfer ",[20,1300,1301],{},"enqueueVariantBatch"," pro Variante — die einzige Stelle, die ",[20,1304,22],{},"-Zeilen anlegt und ",[20,1307,1014],{}," einplant.",[60,1310,1312],{"id":1311},"emailsends-datensätze-und-die-ever-reached-statistiksemantik","emailSends-Datensätze und die „ever-reached\"-Statistiksemantik",[11,1314,1315,1316,1318,1319,1322,1323,1326,1327,1329,1330,1332],{},"Jeder Empfänger einer Kampagne erhält eine ",[20,1317,22],{},"-Zeile (",[20,1320,1321],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fcampaigns.ts","). E-Mail-Adresse, Vor- und Nachname des Kontakts werden zum Versandzeitpunkt ",[15,1324,1325],{},"gesnapshottet"," und nie aktualisiert — die Zeile ist der Audit-Trail dessen, was tatsächlich gesendet wurde, nicht eine Live-Ansicht des Kontakts. ",[20,1328,49],{}," ist ein einzelnes Feld mit acht Werten; ",[20,1331,619],{}," ist der einzige nicht terminale.",[113,1334,1335,1345],{},[116,1336,1337],{},[119,1338,1339,1343],{},[122,1340,1341],{},[20,1342,49],{},[122,1344,127],{},[129,1346,1347,1356,1365,1375,1385,1395,1405,1426],{},[119,1348,1349,1353],{},[134,1350,1351],{},[20,1352,619],{},[134,1354,1355],{},"Eingereiht, noch nicht an den Provider übergeben.",[119,1357,1358,1362],{},[134,1359,1360],{},[20,1361,168],{},[134,1363,1364],{},"Vom Provider angenommen.",[119,1366,1367,1372],{},[134,1368,1369],{},[20,1370,1371],{},"failed",[134,1373,1374],{},"Die Workpool-Action lief auf einen Fehler (nicht dasselbe wie ein Bounce).",[119,1376,1377,1382],{},[134,1378,1379],{},[20,1380,1381],{},"delivered",[134,1383,1384],{},"Provider hat die Zustellung bestätigt.",[119,1386,1387,1392],{},[134,1388,1389],{},[20,1390,1391],{},"opened",[134,1393,1394],{},"Eine Öffnung wurde erfasst.",[119,1396,1397,1402],{},[134,1398,1399],{},[20,1400,1401],{},"clicked",[134,1403,1404],{},"Ein Link-Klick wurde erfasst.",[119,1406,1407,1412],{},[134,1408,1409],{},[20,1410,1411],{},"bounced",[134,1413,1414,1415,1418,1419,358,1422,1425],{},"Provider hat angenommen, aber der Empfangsserver hat abgelehnt. ",[20,1416,1417],{},"bounceType"," hält ",[20,1420,1421],{},"hard",[20,1423,1424],{},"soft"," fest.",[119,1427,1428,1433],{},[134,1429,1430],{},[20,1431,1432],{},"complained",[134,1434,1435],{},"Empfänger hat die Mail als Spam markiert.",[11,1437,1438,1439,1442,1443,1446,1447,1450,1451,1453,1454,1457,1458,1461],{},"Statusschreibungen laufen über einen Schreiber — ",[20,1440,1441],{},"internal.delivery.sendLifecycle.transition"," mit einem ",[20,1444,1445],{},"SendRef { kind: 'campaign', id }",". Die alten Mutationen pro Ereignis in ",[20,1448,1449],{},"delivery\u002Fsends.ts"," (dem Mutationsmodul der ",[20,1452,22],{},"-Tabelle) wurden entfernt; dieses Modul liest nur noch, legt an (einzeln über ",[20,1455,1456],{},"create"," und gebündelt über ",[20,1459,1460],{},"createBatch",") und löscht.",[36,1463,1465],{"title":1464,"type":324},"Statistiken sind „ever-reached\", nicht der aktuelle Status",[11,1466,1467,1470,1471,358,1473,358,1475,335,1477,1479,1480,1482,1483,1485,1486,1488,1489,1491,1492,1494,1495,1497,1498,1500,1501,1504,1505,1507,1508,89,1511,836,1514,1517,1518,1507,1520,1517,1522,1507,1524,1526,1527,1530],{},[20,1468,1469],{},"getStatsByCampaign"," zählt ",[20,1472,1381],{},[20,1474,1391],{},[20,1476,1401],{},[15,1478,1043],{}," über den aktuellen ",[20,1481,49],{},". Weil der ",[20,1484,49],{}," einer Zeile mit neuen Ereignissen weiterrückt (aus einer ",[20,1487,1381],{},"-Zeile wird ",[20,1490,1391],{},", eine ",[20,1493,1391],{},"-Zeile kann später ",[20,1496,1411],{}," werden), würde ein Zählen nach ",[20,1499,49],{}," jeden Empfänger stillschweigend verlieren, der über einen Bucket hinaus fortgeschritten ist, und jeden Ratennenner kaputtmachen. Stattdessen werden diese Buckets aus ",[15,1502,1503],{},"monotonen Zeitstempeln"," abgeleitet: ",[20,1506,1381],{}," zählt jede Zeile mit einem ",[20,1509,1510],{},"deliveredAt",[20,1512,1513],{},"openedAt",[20,1515,1516],{},"clickedAt","; ",[20,1519,1391],{},[20,1521,1513],{},[20,1523,1401],{},[20,1525,1516],{}," oder nicht leeren ",[20,1528,1529],{},"clickedLinks",". Derselbe „jemals zugestellt\"-Nenner wird für die A\u002FB-Statistiken pro Variante verwendet, sodass die beiden Oberflächen nicht auseinanderdriften können.",[11,1532,1533,1535,1536,1539,1540,1543],{},[20,1534,1469],{}," und die anderen Aggregat-Reads pro Send begrenzen ihre Scans (typischerweise ",[20,1537,1538],{},"take(10_000)","); Kampagnen, die größer sind, sollten sich auf die denormalisierten ",[20,1541,1542],{},"stats*","-Zähler an der Kampagnenzeile stützen, die der Send-Lebenszyklus pro Empfänger hochzählt.",[60,1545,1547],{"id":1546},"kampagnen-vs-transaktions-workpools-und-rate-limiting","Kampagnen- vs. Transaktions-Workpools und Rate Limiting",[11,1549,1550,1551,1554],{},"E-Mail-Sends laufen über zwei getrennte Convex-Workpools, definiert in ",[20,1552,1553],{},"apps\u002Fapi\u002Fconvex\u002Fdelivery\u002Fworkpool.ts",":",[113,1556,1557,1572],{},[116,1558,1559],{},[119,1560,1561,1564,1569],{},[122,1562,1563],{},"Pool",[122,1565,1566],{},[20,1567,1568],{},"maxParallelism",[122,1570,1571],{},"Verwendet für",[129,1573,1574,1592],{},[119,1575,1576,1581,1584],{},[134,1577,1578],{},[20,1579,1580],{},"transactionalEmailPool",[134,1582,1583],{},"30\u002FSek.",[134,1585,1586,1587,1591],{},"Zeitkritische transaktionale E-Mails (",[25,1588,1590],{"href":1589},"\u002Fapi\u002Ftransactional","Transactional API",")",[119,1593,1594,1599,1602],{},[134,1595,1596],{},[20,1597,1598],{},"campaignEmailPool",[134,1600,1601],{},"20\u002FSek.",[134,1603,1604],{},"Massenversand von Marketingkampagnen",[11,1606,1607,1608,1611,1612,1615,1616,1619,1620,1623,1624,1626],{},"Zwei Pools verhindern, dass Transaktionsmail hinter einer langen Kampagnen-Queue blockiert wird. Die kombinierten ~50\u002FSek. bleiben mit Sicherheitsmarge unter den Rate-Obergrenzen der Provider. Beide Pools setzen ",[20,1609,1610],{},"retryActionsByDefault",", aber ",[20,1613,1614],{},"maxAttempts: 1"," — genau ein Worker-Lauf und ",[15,1617,1618],{},"kein"," Retry auf Pool-Ebene. Die versandseitige Retry-Schleife gehört ausschließlich dem Dispatch-Helfer (",[20,1621,1622],{},"lib\u002FsendProviders\u002Fdispatch.ts",", ADR-0020); ein Pool-Retry würde den ganzen Worker erneut ausführen und Doppelversand riskieren. Die Felder für 1 s Basis und Faktor 2 im Backoff existieren, werden bei ",[20,1625,1614],{}," aber nie ausgeübt.",[11,1628,1629,1631,1632,1634,1635,1638,1639,1642,1643,1646,1647,1650,1651,1654,1655,358,1658,1661],{},[20,1630,1301],{}," des Orchestrators plant ",[20,1633,1014],{}," ein, das ",[20,1636,1637],{},"campaignEmailPool.enqueueAction"," einmal pro Empfänger aufruft und dabei auf ",[20,1640,1641],{},"internal.delivery.worker.sendSingleEmail"," zielt. Jedes Enqueue verdrahtet den Completion-Callback auf ",[20,1644,1645],{},"internal.delivery.sendCompletion.completeSend"," und trägt eine typisierte ",[20,1648,1649],{},"sendRef"," im Workpool-Kontext, damit das Completion-Modul Worker-Ergebnisse einheitlich in Send-Lebenszyklus-Übergänge übersetzen kann. Standard-Sends werden sofort in Blöcken zu 50 eingeplant; zeitzonenbewusste Sends (",[20,1652,1653],{},"useRecipientTimezone"," plus ",[20,1656,1657],{},"scheduledHour",[20,1659,1660],{},"scheduledMinute",") werden nach IANA-Zeitzone gruppiert und so verzögert, dass jede Zone zur lokalen Zeit der Empfänger landet (DST-korrekt, nicht offsetbasiert).",[60,1663,1665],{"id":1664},"weiterführendes","Weiterführendes",[1667,1668],"link-card",{"description":1669,"title":33,"to":32},"Wie eine Themen- oder Segment-Zielgruppe zu berechtigten Empfängern aufgelöst wird.",[1667,1671],{"description":1672,"title":1673,"to":1674},"Der anwenderseitige Leitfaden zum Konfigurieren und Lesen von A\u002FB-Test-Kampagnen.","A\u002FB-Testing","\u002Fguide\u002Fab-testing",[1667,1676],{"description":1677,"title":1678,"to":1679},"Verzeichnisaufbau, secure-by-default Function Builder und Konventionen.","Convex-Backend","\u002Fdeveloper\u002Fconvex",{"title":1681,"searchDepth":1682,"depth":1682,"links":1683},"",2,[1684,1692,1693,1694,1705,1706,1707],{"id":62,"depth":1682,"text":63,"children":1685},[1686,1688,1689,1690],{"id":110,"depth":1687,"text":111},3,{"id":199,"depth":1687,"text":200},{"id":369,"depth":1687,"text":370},{"id":535,"depth":1687,"text":1691},"sent erreichen: Batch-Abschluss",{"id":625,"depth":1682,"text":626},{"id":829,"depth":1682,"text":830},{"id":983,"depth":1682,"text":984,"children":1695},[1696,1697,1698,1700,1701,1702,1703,1704],{"id":1021,"depth":1687,"text":1022},{"id":1054,"depth":1687,"text":1055},{"id":1067,"depth":1687,"text":1699},"Umschalten auf sending",{"id":1086,"depth":1687,"text":1087},{"id":1141,"depth":1687,"text":1142},{"id":1168,"depth":1687,"text":1169},{"id":1178,"depth":1687,"text":1179},{"id":1239,"depth":1687,"text":1240},{"id":1311,"depth":1682,"text":1312},{"id":1546,"depth":1682,"text":1547},{"id":1664,"depth":1682,"text":1665},"Wie das Kampagnen-Backend arbeitet: zwei Statusmaschinen, Versand-Pre-Flight, der Versand-Orchestrator, emailSends-Datensätze und die priorisierten Workpools.","md",{},true,"\u002Fdeveloper\u002Fcampaign-internals",{"title":6,"description":1708},"3.developer\u002F16.campaign-internals","Dz9TG_drDgRh6zAft3v2ZkGRB_gGiuXrMvtxOiDEQNw",[1717,1721],{"title":1718,"path":1719,"stem":1720,"children":-1},"Provider","\u002Fdeveloper\u002Fproviders","3.developer\u002F15.providers",{"title":33,"path":32,"stem":1722,"children":-1},"3.developer\u002F17.audience-internals",1786915108810]