|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace DrewM\MailChimp; |
|
4
|
|
|
|
|
5
|
|
|
/** |
|
6
|
|
|
* An error handler for MailChimp. |
|
7
|
|
|
* Help MailChimp error classes, categorization |
|
8
|
|
|
* and determinate whether a failure is, eg soft/hard. |
|
9
|
|
|
* |
|
10
|
|
|
* Example: |
|
11
|
|
|
* |
|
12
|
|
|
* if ($code & ErrorHandler::ALREADY_EXISTS) { |
|
13
|
|
|
* // return "ok"; |
|
14
|
|
|
* } elseif ($code & ErrorHandler::SOFT_FAILURE) { |
|
15
|
|
|
* // return "ok"; |
|
16
|
|
|
* } elseif ($code & (ErrorHandler::NET_TIMEOUT | ErrorHandler::NET_NO_JSON)) { |
|
17
|
|
|
* // return "retry", |
|
18
|
|
|
* } |
|
19
|
|
|
* |
|
20
|
|
|
* @author Raphaël Droz <[email protected]> |
|
21
|
|
|
* |
|
22
|
|
|
*/ |
|
23
|
|
|
class ErrorHandler |
|
24
|
|
|
{ |
|
25
|
|
|
// General return categories: OK or failure (By default, failures are most probably API rejections) |
|
26
|
|
|
const OK = 1 << 0; |
|
27
|
|
|
const FAILURE = 1 << 1; |
|
28
|
|
|
|
|
29
|
|
|
// "Network" failure "subcategory" |
|
30
|
|
|
const NET_FAILURE = 1 << 2; |
|
31
|
|
|
|
|
32
|
|
|
// Kind of failure "tag". Some network or API errors are judged "soft" or "temporary". |
|
33
|
|
|
// It's up to the callee to test SOFT_FAILURE congruence and make use of this information if desired. |
|
34
|
|
|
const SOFT_FAILURE = 1 << 3; |
|
35
|
|
|
|
|
36
|
|
|
// successes |
|
37
|
|
|
const INSERTED = 1 << 6; |
|
38
|
|
|
const UPDATED = 1 << 7; |
|
39
|
|
|
const DELETED = 1 << 8; |
|
40
|
|
|
|
|
41
|
|
|
// api rejections |
|
42
|
|
|
const INVALID_RES = 1 << 10; // usually from a programmer error |
|
43
|
|
|
const INVALID_API_KEY = 1 << 11; // idem |
|
44
|
|
|
const ALREADY_EXISTS = 1 << 12; |
|
45
|
|
|
const COMPLIANCE = 1 << 13; |
|
46
|
|
|
const FAKE = 1 << 14; |
|
47
|
|
|
const INVALID_DOMAIN = 1 << 15; |
|
48
|
|
|
const CAPPED = 1 << 16; |
|
49
|
|
|
const API_OTHER = 1 << 17; |
|
50
|
|
|
const INVALID_ADDRESS = 1 << 18; // invalid ADDRESS merge field |
|
51
|
|
|
|
|
52
|
|
|
// network failures |
|
53
|
|
|
const NET_REQ_PROC = 1 << 20; // Neither permanent nor record-specific error. try again later. |
|
54
|
|
|
const NET_TIMEOUT = 1 << 21; // curl: Operation timed out |
|
55
|
|
|
const NET_NO_JSON = 1 << 22; // non-JSON response |
|
56
|
|
|
const NET_UNKNOWN = 1 << 23; |
|
57
|
|
|
const UNKNOWN_ERROR = 1 << 24; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* A couple of shortcuts to directly return a specific error of a given category |
|
61
|
|
|
*/ |
|
62
|
|
|
private function ok(int $r, array $a) |
|
63
|
|
|
{ |
|
64
|
|
|
return $this->log($r | self::OK, $a); |
|
65
|
|
|
} |
|
66
|
|
|
private function fail(int $r, array $a) |
|
67
|
|
|
{ |
|
68
|
|
|
return $this->log($r | self::FAILURE, $a); |
|
69
|
|
|
} |
|
70
|
|
|
private function netfail(int $r, array $a) |
|
71
|
|
|
{ |
|
72
|
|
|
return $this->fail($r | self::NET_FAILURE, $a); |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* A list of well-known failures. |
|
77
|
|
|
* They are tested in order. Put more restrictive first |
|
78
|
|
|
* At the step where these are probed, |
|
79
|
|
|
* response is certainly a *failure*, as such all below return codes must be also be OR-ed with self::FAILURE |
|
80
|
|
|
*/ |
|
81
|
|
|
const CODES = [ |
|
82
|
|
|
'400' => [ |
|
83
|
|
|
// See https://developer.mailchimp.com/documentation/mailchimp/guides/error-glossary/ for categories |
|
84
|
|
|
// 'BadRequest', // Your request could not be processed. |
|
85
|
|
|
// 'InvalidAction', |
|
86
|
|
|
// 'InvalidResource', // The resource submitted could not be validated. |
|
87
|
|
|
// 'JSONParseError', |
|
88
|
|
|
['code' => self::ALREADY_EXISTS, |
|
89
|
|
|
'tests' => [ 'title' => 'Member Exists', |
|
90
|
|
|
'detail' => 'is already a list member']], |
|
91
|
|
|
|
|
92
|
|
|
['code' => self::COMPLIANCE, |
|
93
|
|
|
'tests' => ['title' => 'Member In Compliance State', |
|
94
|
|
|
'detail' => 'is in a compliance state due to unsubscribe, bounce, or compliance review']], |
|
95
|
|
|
|
|
96
|
|
|
['code' => self::INVALID_API_KEY | self::SOFT_FAILURE, |
|
97
|
|
|
'tests' => ['title' => 'API Key Invalid']], |
|
98
|
|
|
|
|
99
|
|
|
['code' => self::FAKE, |
|
100
|
|
|
'tests' => ['title' => 'Invalid Resource', |
|
101
|
|
|
'detail' => 'looks fake or invalid']], |
|
102
|
|
|
|
|
103
|
|
|
['code' => self::INVALID_ADDRESS, |
|
104
|
|
|
'tests' => ['title' => 'Invalid Resource', |
|
105
|
|
|
'detail' => 'Your merge fields were invalid', |
|
106
|
|
|
'errors' => 'Please enter a complete address']], |
|
107
|
|
|
|
|
108
|
|
|
['code' => self::INVALID_DOMAIN, |
|
109
|
|
|
'tests' => ['title' => 'Invalid Resource', |
|
110
|
|
|
'detail' => 'domain portion of the email address is invalid']], |
|
111
|
|
|
|
|
112
|
|
|
['code' => self::CAPPED, |
|
113
|
|
|
'tests' => ['title' => 'Invalid Resource', |
|
114
|
|
|
'detail' => 'has signed up to a lot of lists very recently']], |
|
115
|
|
|
|
|
116
|
|
|
['code' => self::INVALID_RES | self::SOFT_FAILURE, |
|
117
|
|
|
'tests' => ['title' => 'Invalid Resource', |
|
118
|
|
|
'detail' => 'The resource submitted could not be validated']], |
|
119
|
|
|
], |
|
120
|
|
|
/* |
|
121
|
|
|
'401' => [ |
|
122
|
|
|
'APIKeyMissing', |
|
123
|
|
|
'APIKeyInvalid', |
|
124
|
|
|
], |
|
125
|
|
|
'403' => [ |
|
126
|
|
|
'Forbidden', |
|
127
|
|
|
'UserDisabled', |
|
128
|
|
|
'WrongDatacenter' |
|
129
|
|
|
], |
|
130
|
|
|
'404' => [ |
|
131
|
|
|
'ResourceNotFound' |
|
132
|
|
|
] |
|
133
|
|
|
'405' => [ |
|
134
|
|
|
'MethodNotAllowed' |
|
135
|
|
|
], |
|
136
|
|
|
'414' => [ |
|
137
|
|
|
'ResourceNestingTooDeep' |
|
138
|
|
|
], |
|
139
|
|
|
'422' => [ |
|
140
|
|
|
'InvalidMethodOverride' |
|
141
|
|
|
], |
|
142
|
|
|
'429' => [ |
|
143
|
|
|
'TooManyRequests' |
|
144
|
|
|
], |
|
145
|
|
|
'500' => [ |
|
146
|
|
|
'InternalServerError' |
|
147
|
|
|
], |
|
148
|
|
|
'503' => [ |
|
149
|
|
|
'ComplianceRelated' |
|
150
|
|
|
] |
|
151
|
|
|
*/ |
|
152
|
|
|
]; |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* Simple categorization, exclusively used for logging. |
|
156
|
|
|
*/ |
|
157
|
|
|
const ERROR_CLASSES = [ |
|
158
|
|
|
self::COMPLIANCE => 'compliance-state', |
|
159
|
|
|
self::FAKE => 'fake', |
|
160
|
|
|
self::INVALID_DOMAIN => 'invalid-domain', |
|
161
|
|
|
self::CAPPED => 'capped' |
|
162
|
|
|
]; |
|
163
|
|
|
|
|
164
|
|
|
// Unused |
|
165
|
|
|
const MC_RETURN_CODES = [ ['code' => -100, 'name' => 'ValidationError'], |
|
166
|
|
|
['code' => -99, 'name' => 'List_RoleEmailMember'], |
|
167
|
|
|
['code' => 212, 'name' => 'List_InvalidUnsubMember'], |
|
168
|
|
|
['code' => 213, 'name' => 'List_InvalidBounceMember'], |
|
169
|
|
|
['code' => 234, 'name' => 'List_ThrottledRecipient'] ]; |
|
170
|
|
|
|
|
171
|
|
|
protected $logger; |
|
172
|
|
|
|
|
173
|
|
|
public function __construct(?\Psr\Log\LoggerInterface $logger) |
|
174
|
|
|
{ |
|
175
|
|
|
if ($logger) { |
|
176
|
|
|
$this->logger = $logger; |
|
177
|
|
|
} else { |
|
178
|
|
|
$this->logger = new class |
|
179
|
|
|
{ |
|
180
|
|
|
public function __call($name, $args) |
|
181
|
|
|
{ |
|
182
|
|
|
printf('[%s] %s' . PHP_EOL, strtoupper($name), $args[0]); |
|
183
|
|
|
} |
|
184
|
|
|
}; |
|
185
|
|
|
} |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
/** |
|
189
|
|
|
* @param MailChimp $mailchimp A MailChimp class. |
|
190
|
|
|
* @param array $json_response The MailChimp response. |
|
191
|
|
|
* @param string $method The method used in the request. |
|
192
|
|
|
* @param array $loginfo Additional parameter(s) to pass as logging variables. |
|
193
|
|
|
* |
|
194
|
|
|
* Example: |
|
195
|
|
|
* |
|
196
|
|
|
* $response = $mc->post($url, $some_data); |
|
197
|
|
|
* return $errorHandler->errno($mc, $response, 'POST', ['id' => $request_id]) |
|
198
|
|
|
* |
|
199
|
|
|
*/ |
|
200
|
|
|
public function errno(MailChimp $mailchimp, array $json_response, string $method, array $loginfo = []) |
|
201
|
|
|
{ |
|
202
|
|
|
$http_code = $mailchimp->getLastResponse()['headers']['http_code']; |
|
203
|
|
|
$body = $mailchimp->getLastResponse()['body']; |
|
204
|
|
|
$detail = $json_response['detail'] ?? ''; |
|
|
|
|
|
|
205
|
|
|
// $logargs is just an array of arguments which improve error logging |
|
206
|
|
|
$logargs = compact('json_response', 'mailchimp') + $loginfo; |
|
207
|
|
|
|
|
208
|
|
|
/** |
|
209
|
|
|
* Handles anything from network to webserver failures through content-type problems. |
|
210
|
|
|
* Basically any case where no valid JSON is not returned with a 2xx code. |
|
211
|
|
|
*/ |
|
212
|
|
|
if (! $json_response) { |
|
|
|
|
|
|
213
|
|
|
// Not an error, just a response to DELETE |
|
214
|
|
|
if ($method === 'DELETE' && $http_code === 204) { |
|
215
|
|
|
return $this->ok(self::DELETED, $logargs); |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
if (strpos($body, 'An error occurred while processing your request') !== false) { |
|
219
|
|
|
return $this->netfail(self::NET_NO_JSON | self::SOFT_FAILURE | self::NET_REQ_PROC, $logargs); |
|
220
|
|
|
} elseif ($body) { |
|
221
|
|
|
return $this->netfail(self::NET_NO_JSON, $logargs); |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
if (strpos($mailchimp->getLastError(), 'Operation timed out') === 0) { |
|
225
|
|
|
return $this->netfail(self::NET_TIMEOUT | self::SOFT_FAILURE, $logargs); |
|
226
|
|
|
} |
|
227
|
|
|
return $this->netfail($mailchimp->getLastError() ? self::NET_FAILURE : self::NET_UNKNOWN, $logargs); |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
if (empty($json_response['status'])) { |
|
231
|
|
|
return $this->fail(self::UNKNOWN_ERROR, $logargs); |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
if (!empty($json_response['email_address'])) { |
|
235
|
|
|
if ($method === 'POST') { |
|
236
|
|
|
return $this->ok(self::INSERTED, $logargs); |
|
237
|
|
|
} |
|
238
|
|
|
if ($method === 'PUT') { |
|
239
|
|
|
return $this->ok(self::UPDATED, $logargs); |
|
240
|
|
|
} |
|
241
|
|
|
if ($method === 'PATCH') { |
|
242
|
|
|
return $this->ok(self::UPDATED, $logargs); |
|
243
|
|
|
} |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* Attempt to "recognize" a MailChimp API error: |
|
248
|
|
|
* "status" could very well be a "real" MailChimp status, like in "status": "unsubscribed" |
|
249
|
|
|
* (and "unsubscribed" != 200) |
|
250
|
|
|
* In order to detect an error, response must be: |
|
251
|
|
|
* 1. numeric |
|
252
|
|
|
* 2. code != 200 |
|
253
|
|
|
* 3. "detail" key must be present |
|
254
|
|
|
*/ |
|
255
|
|
|
$isFailed = is_numeric($json_response['status']) |
|
256
|
|
|
&& $json_response['status'] != '200' |
|
257
|
|
|
&& isset($json_response['detail']); |
|
258
|
|
|
|
|
259
|
|
|
if (! $isFailed) { |
|
260
|
|
|
var_dump("No place for success in this codepath. api-wtf.", $json_response); |
|
|
|
|
|
|
261
|
|
|
return $this->fail(self::UNKNOWN_ERROR, $logargs); |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
foreach (self::CODES as $status_code => $errors) { |
|
265
|
|
|
// Note self::CODES we define JSON response "status" codes. Anyway it does not seem needed to test for them. |
|
266
|
|
|
foreach ($errors as $error) { |
|
267
|
|
|
extract($error); // defines $code and $tests |
|
268
|
|
|
if (!$tests) { |
|
269
|
|
|
continue; |
|
270
|
|
|
} |
|
271
|
|
|
$match = true; |
|
272
|
|
|
foreach ($tests as $field => $val) { |
|
273
|
|
|
if ($json_response[$field] === $val |
|
274
|
|
|
|| ($field === 'detail' && strpos($json_response['detail'], $val) !== false) |
|
275
|
|
|
|| ($field === 'errors' && strpos(json_encode($json_response['errors']), $val) !== false)) { |
|
276
|
|
|
// pass |
|
277
|
|
|
} else { |
|
278
|
|
|
$match = false; |
|
279
|
|
|
break; |
|
280
|
|
|
} |
|
281
|
|
|
} |
|
282
|
|
|
if ($match) { |
|
283
|
|
|
return $this->fail($code, $logargs); |
|
284
|
|
|
} |
|
285
|
|
|
} |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
return $this->fail(self::API_OTHER, $logargs); |
|
289
|
|
|
} |
|
290
|
|
|
|
|
291
|
|
|
/** |
|
292
|
|
|
* Logging API errors into the database is wrong. |
|
293
|
|
|
* It's the role of our logger to perform well enough |
|
294
|
|
|
*/ |
|
295
|
|
|
public function log(int $code, array $logargs) |
|
296
|
|
|
{ |
|
297
|
|
|
$logger = $this->logger; |
|
298
|
|
|
extract($logargs); // defines $id, $json_response, $mailchimp |
|
299
|
|
|
|
|
300
|
|
|
$last_request = self::obfuscateRequest($mailchimp->getLastRequest()); |
|
301
|
|
|
$last_response = self::obfuscateRequest($mailchimp->getLastResponse()); |
|
302
|
|
|
|
|
303
|
|
|
$data = [ |
|
304
|
|
|
'code' => $code, |
|
305
|
|
|
'method' => $last_request['method'], |
|
306
|
|
|
'substatus' => $last_request['body']['status'] ?? '', |
|
307
|
|
|
'email' => $last_request['body']['email_address'] ?? '', |
|
308
|
|
|
'http_code' => $last_response['headers']['http_code'], |
|
309
|
|
|
'json_response' => json_encode($json_response), |
|
310
|
|
|
'mc_error' => $mailchimp->getLastError(), |
|
311
|
|
|
]; |
|
312
|
|
|
|
|
313
|
|
|
$prefix = 'mailchimp-api {method}'; |
|
314
|
|
|
if (!empty($id)) { |
|
315
|
|
|
$prefix .= ', id={id}'; |
|
316
|
|
|
$data['id'] = $id; |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
if ($code & self::OK) { |
|
320
|
|
|
$logger->debug(self::interpolate($prefix . ': OK', $data)); |
|
|
|
|
|
|
321
|
|
|
return $code; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
if (!empty($id) && !empty($data['email'])) { |
|
325
|
|
|
$prefix .= '/{email}'; |
|
326
|
|
|
} |
|
327
|
|
|
|
|
328
|
|
|
// Grab corresponding error class as string. |
|
329
|
|
|
$type = array_filter(self::ERROR_CLASSES, function ($string, $return_code) use ($code) { |
|
330
|
|
|
return $code & $return_code; |
|
331
|
|
|
}, ARRAY_FILTER_USE_BOTH); |
|
332
|
|
|
$type = array_shift($type); |
|
333
|
|
|
if ($type) { |
|
334
|
|
|
$prefix .= ' [{type}]'; |
|
335
|
|
|
$data['type'] = $type; |
|
336
|
|
|
} |
|
337
|
|
|
// import `json_response` and request (like `method`) keys to be usable in the logs |
|
338
|
|
|
$data += $json_response + $last_request; |
|
339
|
|
|
$data['method'] = strtoupper($data['method']); |
|
340
|
|
|
|
|
341
|
|
|
if ($code & self::NET_FAILURE) { |
|
342
|
|
|
$prefix .= ': http={http_code} network error'; |
|
343
|
|
|
if ($code & self::SOFT_FAILURE) { |
|
344
|
|
|
$prefix .= '. [Temporary] Reason: {mc_error}'; |
|
345
|
|
|
} |
|
346
|
|
|
} |
|
347
|
|
|
if ($code & self::NET_NO_JSON) { |
|
348
|
|
|
$logger->error(self::interpolate($prefix . ': json={json_response}, body={body}', $data)); |
|
|
|
|
|
|
349
|
|
|
} elseif (($r['status'] ?? null) == '400') { |
|
350
|
|
|
$logger->warning(self::interpolate($prefix . ': {substatus} other error: {response}', $data)); |
|
|
|
|
|
|
351
|
|
|
} |
|
352
|
|
|
if ($code & self::ALREADY_EXISTS) { |
|
353
|
|
|
$data['detail'] = preg_replace('/Use PUT.*/', '', $data['detail']); |
|
354
|
|
|
$logger->debug(self::interpolate($prefix . ': {detail}', $data)); |
|
|
|
|
|
|
355
|
|
|
return $code; |
|
356
|
|
|
} elseif ($code & self::INVALID_ADDRESS) { |
|
357
|
|
|
$logger->warning(self::interpolate($prefix . ': Invalid address: {body}', $data)); |
|
|
|
|
|
|
358
|
|
|
return $code; |
|
359
|
|
|
} |
|
360
|
|
|
|
|
361
|
|
|
// Dump as much as possible to further improve and predict errors |
|
362
|
|
|
if ($code & self::FAILURE) { |
|
363
|
|
|
if ($code & self::SOFT_FAILURE) { |
|
364
|
|
|
$logger->debug(self::interpolate($prefix . ': soft-fail {code}', $data)); |
|
|
|
|
|
|
365
|
|
|
} else { |
|
366
|
|
|
$logger->warning(self::interpolate($prefix . ': status={status},title={title},detail={detail}', $data)); |
|
|
|
|
|
|
367
|
|
|
} |
|
368
|
|
|
if (!$type) { |
|
369
|
|
|
$logger->debug(self::interpolate('{request} ++ {response} -- {json}', ['request' => $last_request, |
|
|
|
|
|
|
370
|
|
|
'response' => $last_response, |
|
371
|
|
|
'json' => $json_response])); |
|
372
|
|
|
} |
|
373
|
|
|
} |
|
374
|
|
|
return $code; |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
/** |
|
378
|
|
|
* Monolog default LineFormatter appends the whole context at the end of the log line. |
|
379
|
|
|
* Even if elements are used as message placeholders. This (PSR-3) interpolation function |
|
380
|
|
|
* avoids this. |
|
381
|
|
|
*/ |
|
382
|
|
|
private static function interpolate(string $message, array $context = []) |
|
383
|
|
|
{ |
|
384
|
|
|
// build a replacement array with braces around the context keys |
|
385
|
|
|
$replace = []; |
|
386
|
|
|
foreach ($context as $key => $val) { |
|
387
|
|
|
// check that the value can be casted to string |
|
388
|
|
|
if (!is_array($val) && !is_object($val) || method_exists($val, '__toString')) { |
|
389
|
|
|
$replace['{' . $key . '}'] = $val; |
|
390
|
|
|
} else { |
|
391
|
|
|
$replace['{' . $key . '}'] = json_encode($val); |
|
392
|
|
|
} |
|
393
|
|
|
} |
|
394
|
|
|
|
|
395
|
|
|
// interpolate replacement values into the message and return |
|
396
|
|
|
return strtr($message, $replace); |
|
397
|
|
|
} |
|
398
|
|
|
|
|
399
|
|
|
public static function obfuscateRequest($request) |
|
400
|
|
|
{ |
|
401
|
|
|
if (!isset($request['headers'])) { |
|
402
|
|
|
return $request; |
|
403
|
|
|
} |
|
404
|
|
|
|
|
405
|
|
|
if (isset($request['headers']['request_header'])) { |
|
406
|
|
|
$request['headers']['request_header'] = preg_replace( |
|
407
|
|
|
'/[0-9a-f]{32}(-us\d+)/', |
|
408
|
|
|
str_repeat('*', 32) . '\1', |
|
409
|
|
|
$request['headers']['request_header'] |
|
410
|
|
|
); |
|
411
|
|
|
} elseif (is_string($request['headers'])) { |
|
412
|
|
|
$request['headers'] = preg_replace( |
|
413
|
|
|
'/apikey [0-9a-f]{32}(-us\d+)/', |
|
414
|
|
|
'apikey ' . str_repeat('*', 32) . '\1', |
|
415
|
|
|
$request['headers'] |
|
416
|
|
|
); |
|
417
|
|
|
} |
|
418
|
|
|
|
|
419
|
|
|
return $request; |
|
420
|
|
|
} |
|
421
|
|
|
} |
|
422
|
|
|
|
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
Both the
$myVarassignment in line 1 and the$higherassignment in line 2 are dead. The first because$myVaris never used and the second because$higheris always overwritten for every possible time line.