Skip to content

Commit 4d6a6b0

Browse files
authored
feat: Emit migration op event (#136)
1 parent 20e80fb commit 4d6a6b0

File tree

3 files changed

+237
-13
lines changed

3 files changed

+237
-13
lines changed

src/LaunchDarkly/LDClient.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,30 @@ public function track(string $eventName, LDContext $context, mixed $data = null,
393393
$this->_eventProcessor->enqueue($this->_eventFactoryDefault->newCustomEvent($eventName, $context, $data, $metricValue));
394394
}
395395

396+
/**
397+
* Tracks the results of a migrations operation. This event includes
398+
* measurements which can be used to enhance the observability of a
399+
* migration within the LaunchDarkly UI.
400+
*
401+
* Customers making use of the {@see
402+
* LaunchDarkly\Migrations\MigrationBuilder} should not need to call this
403+
* method manually.
404+
*
405+
* Customers not using the builder should provide this method with the
406+
* tracker returned from calling {@ LDClient::migrationVariation}.
407+
*/
408+
public function trackMigrationOperation(OpTracker $tracker): void
409+
{
410+
$event = $tracker->build();
411+
412+
if (is_string($event)) {
413+
$this->_logger->error("error generating migration op event {$event}; no event will be emitted");
414+
return;
415+
}
416+
417+
$this->_eventProcessor->enqueue($event);
418+
}
419+
396420
/**
397421
* Reports details about an evaluation context.
398422
*

src/LaunchDarkly/Migrations/Migrator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function read(
5454
Stage::COMPLETE => $new->run(),
5555
};
5656

57-
// TODO(sc-219377): Emit the event here
57+
$this->client->trackMigrationOperation($tracker);
5858

5959
return $result;
6060
}
@@ -87,7 +87,7 @@ public function write(
8787
Stage::COMPLETE => new WriteResult($new->run()),
8888
};
8989

90-
// TODO(sc-219377): Emit the event here
90+
$this->client->trackMigrationOperation($tracker);
9191

9292
return $writeResult;
9393
}

tests/Migrations/MigratorTest.php

Lines changed: 211 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
class MigratorTest extends \PHPUnit\Framework\TestCase
1919
{
2020
private MigratorBuilder $builder;
21+
private MockEventProcessor $eventProcessor;
2122

2223
public function setUp(): void
2324
{
@@ -27,9 +28,10 @@ public function setUp(): void
2728
$requester->addFlag($this->makeOffFlagWithValue($stage->value, $stage->value));
2829
}
2930

31+
$this->eventProcessor = new MockEventProcessor();
3032
$options = [
3133
'feature_requester' => $requester,
32-
'event_processor' => new MockEventProcessor()
34+
'event_processor' => $this->eventProcessor,
3335
];
3436

3537
$client = new LDClient("someKey", $options);
@@ -138,23 +140,77 @@ public function readStageOriginProvider(): array
138140
*/
139141
public function testTrackingInvokedForReads(Stage $stage, array $origins): void
140142
{
141-
$this->markTestSkipped('skipped until sc-219378');
143+
/** @var Migrator */
144+
$migrator = $this->builder->build()->value;
145+
$migrator->read($stage->value, LDContext::create('user-key'), Stage::LIVE);
146+
147+
$events = $this->eventProcessor->getEvents();
148+
149+
$this->assertCount(2, $events);
150+
151+
$event = $events[1]; // First event is evaluation result
152+
$invoked = $event['measurements'][0];
153+
154+
$this->assertEquals('invoked', $invoked['key']);
155+
156+
array_map(fn ($origin) => $this->assertTrue($invoked['values'][$origin->value]), $origins);
142157
}
143158

144159
/**
145160
* @dataProvider readStageOriginProvider
146161
*/
147162
public function testTrackingLatencyForReads(Stage $stage, array $origins): void
148163
{
149-
$this->markTestSkipped('skipped until sc-219378');
164+
$delayed = function (mixed $payload): Result {
165+
return Result::success(null);
166+
};
167+
168+
$this->builder->read($delayed, $delayed);
169+
$this->builder->trackLatency(true);
170+
171+
/** @var Migrator */
172+
$migrator = $this->builder->build()->value;
173+
$migrator->read($stage->value, LDContext::create('user-key'), Stage::LIVE);
174+
175+
$events = $this->eventProcessor->getEvents();
176+
177+
$this->assertCount(2, $events);
178+
179+
$event = $events[1]; // First event is evaluation result
180+
$latencies = $event['measurements'][1]; // First measurement is invoked
181+
182+
$this->assertEquals('latency_ms', $latencies['key']);
183+
foreach ($origins as $origin) {
184+
$this->assertGreaterThanOrEqual($latencies['values'][$origin->value], 100);
185+
}
150186
}
151187

152188
/**
153189
* @dataProvider readStageOriginProvider
154190
*/
155191
public function testTrackingErrorsForReads(Stage $stage, array $origins): void
156192
{
157-
$this->markTestSkipped('skipped until sc-219378');
193+
$this->builder->read(
194+
fn () => throw new Exception("old write"),
195+
fn () => throw new Exception("new write"),
196+
);
197+
$this->builder->trackErrors(true);
198+
199+
/** @var Migrator */
200+
$migrator = $this->builder->build()->value;
201+
$migrator->read($stage->value, LDContext::create('user-key'), Stage::LIVE);
202+
203+
$events = $this->eventProcessor->getEvents();
204+
205+
$this->assertCount(2, $events);
206+
207+
$event = $events[1]; // First event is evaluation result
208+
$errors = $event['measurements'][1]; // First measurement is invoked
209+
210+
$this->assertEquals('error', $errors['key']);
211+
foreach ($origins as $origin) {
212+
$this->assertTrue($errors['values'][$origin->value]);
213+
}
158214
}
159215

160216
public function writeStageOriginProvider(): array
@@ -174,28 +230,172 @@ public function writeStageOriginProvider(): array
174230
*/
175231
public function testTrackingInvokedForWrites(Stage $stage, array $origins): void
176232
{
177-
$this->markTestSkipped('skipped until sc-219377');
233+
/** @var Migrator */
234+
$migrator = $this->builder->build()->value;
235+
$migrator->write($stage->value, LDContext::create('user-key'), Stage::LIVE);
236+
237+
$events = $this->eventProcessor->getEvents();
238+
239+
$this->assertCount(2, $events);
240+
241+
$event = $events[1]; // First event is evaluation result
242+
$invoked = $event['measurements'][0];
243+
244+
$this->assertEquals('invoked', $invoked['key']);
245+
246+
array_map(fn ($origin) => $this->assertTrue($invoked['values'][$origin->value]), $origins);
178247
}
179248

180249
/**
181250
* @dataProvider writeStageOriginProvider
182251
*/
183252
public function testTrackingLatencyForWrites(Stage $stage, array $origins): void
184253
{
185-
$this->markTestSkipped('skipped until sc-219377');
254+
$delayed = function (mixed $payload): Result {
255+
return Result::success(null);
256+
};
257+
258+
$this->builder->write($delayed, $delayed);
259+
$this->builder->trackLatency(true);
260+
261+
/** @var Migrator */
262+
$migrator = $this->builder->build()->value;
263+
$migrator->write($stage->value, LDContext::create('user-key'), Stage::LIVE);
264+
265+
$events = $this->eventProcessor->getEvents();
266+
267+
$this->assertCount(2, $events);
268+
269+
$event = $events[1]; // First event is evaluation result
270+
$latencies = $event['measurements'][1]; // First measurement is invoked
271+
272+
$this->assertEquals('latency_ms', $latencies['key']);
273+
foreach ($origins as $origin) {
274+
$this->assertGreaterThanOrEqual($latencies['values'][$origin->value], 100);
275+
}
276+
}
277+
278+
public function authoritativeWriteStageOriginProvider(): array
279+
{
280+
return [
281+
[Stage::OFF, Origin::OLD],
282+
[Stage::DUALWRITE, Origin::OLD],
283+
[Stage::SHADOW, Origin::OLD],
284+
[Stage::LIVE, Origin::NEW],
285+
[Stage::RAMPDOWN, Origin::NEW],
286+
[Stage::COMPLETE, Origin::NEW],
287+
];
186288
}
187289

188290
/**
189-
* @dataProvider writeStageOriginProvider
291+
* @dataProvider authoritativeWriteStageOriginProvider
190292
*/
191-
public function testTrackingErrorsForWrites(Stage $stage, array $origins): void
293+
public function testTrackingErrorsForAuthoritativeWrites(Stage $stage, Origin $origin): void
192294
{
193-
$this->markTestSkipped('skipped until sc-219377');
295+
$this->builder->write(
296+
fn () => throw new Exception("old write"),
297+
fn () => throw new Exception("new write"),
298+
);
299+
$this->builder->trackErrors(true);
300+
301+
/** @var Migrator */
302+
$migrator = $this->builder->build()->value;
303+
$migrator->write($stage->value, LDContext::create('user-key'), Stage::LIVE);
304+
305+
$events = $this->eventProcessor->getEvents();
306+
307+
$this->assertCount(2, $events);
308+
309+
$event = $events[1]; // First event is evaluation result
310+
$errors = $event['measurements'][1]; // First measurement is invoked
311+
312+
$this->assertEquals('error', $errors['key']);
313+
$this->assertTrue($errors['values'][$origin->value]);
314+
}
315+
316+
public function nonauthoritativeWriteStageOriginProvider(): array
317+
{
318+
return [
319+
// Off and Complete only run authoritative writes so there is nothing to test
320+
[Stage::DUALWRITE, Origin::OLD, Origin::NEW],
321+
[Stage::SHADOW, Origin::OLD, Origin::NEW],
322+
[Stage::LIVE, Origin::NEW, Origin::OLD],
323+
[Stage::RAMPDOWN, Origin::NEW, Origin::OLD],
324+
];
325+
}
326+
327+
/**
328+
* @dataProvider nonauthoritativeWriteStageOriginProvider
329+
*/
330+
public function testTrackingErrorsForNonAuthoritativeWrites(Stage $stage, Origin $authoritative, Origin $nonauthoritative): void
331+
{
332+
if ($authoritative == Origin::OLD) {
333+
$this->builder->write(
334+
fn () => Result::success(null),
335+
fn () => throw new Exception("new write"),
336+
);
337+
} else {
338+
$this->builder->write(
339+
fn () => throw new Exception("old write"),
340+
fn () => Result::success(null),
341+
);
342+
}
343+
$this->builder->trackErrors(true);
344+
345+
/** @var Migrator */
346+
$migrator = $this->builder->build()->value;
347+
$migrator->write($stage->value, LDContext::create('user-key'), Stage::LIVE);
348+
349+
$events = $this->eventProcessor->getEvents();
350+
351+
$this->assertCount(2, $events);
352+
353+
$event = $events[1]; // First event is evaluation result
354+
$errors = $event['measurements'][1]; // First measurement is invoked
355+
356+
$this->assertEquals('error', $errors['key']);
357+
$this->assertTrue($errors['values'][$nonauthoritative->value]);
194358
}
195359

196-
public function testTrackingConsistency(): void
360+
public function trackingConsistencyProvider(): array
197361
{
198-
$this->markTestSkipped('skipped until sc-219377');
362+
return [
363+
// SHADOW and LIVE are the only stages that run both reads and as a
364+
// result, can produce consistency values.
365+
[Stage::SHADOW, "same", "same", true],
366+
[Stage::LIVE, "same", "same", true],
367+
368+
[Stage::SHADOW, "different", "same", false],
369+
[Stage::LIVE, "different", "same", false],
370+
];
371+
}
372+
373+
/**
374+
* @dataProvider trackingConsistencyProvider
375+
*/
376+
public function testTrackingConsistency(Stage $stage, string $old, string $new, bool $shouldMatch): void
377+
{
378+
$this->builder->read(
379+
fn () => Result::success($old),
380+
fn () => Result::success($new),
381+
fn ($lhs, $rhs) => $lhs == $rhs,
382+
);
383+
384+
/** @var Migrator */
385+
$migrator = $this->builder->build()->value;
386+
$migrator->read($stage->value, LDContext::create('user-key'), Stage::LIVE);
387+
388+
$events = $this->eventProcessor->getEvents();
389+
390+
$this->assertCount(2, $events);
391+
392+
$event = $events[1]; // First event is evaluation result
393+
$consistency = $event['measurements'][1]; // First measurement is invoked
394+
395+
$this->assertEquals('consistent', $consistency['key']);
396+
$this->assertEquals($shouldMatch, $consistency['value']);
397+
398+
// TODO(sc-219378): Add sampling tests
199399
}
200400

201401
public function readHandlerExceptionProvider(): array

0 commit comments

Comments
 (0)