appconfig-experiment-backfill 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/backfill.js +548 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# appconfig-experiment-backfill
|
|
2
|
+
|
|
3
|
+
Generate mock [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) telemetry for testing [Experimentation](https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-experimentation) on [Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview).
|
|
4
|
+
|
|
5
|
+
This CLI backfills realistic custom events, page views, browser timings, and exception telemetry into an App Insights resource so you can exercise the experimentation analysis pipeline without real user traffic.
|
|
6
|
+
|
|
7
|
+
> **Note** — This is a personal tool by [@rossgrambo](https://github.com/rossgrambo), not an official Azure App Configuration product.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx appconfig-experiment-backfill \
|
|
13
|
+
--flag=button-display \
|
|
14
|
+
--feature-flag-reference="https://..." \
|
|
15
|
+
--variants=get-started,sign-up \
|
|
16
|
+
--connectionString="InstrumentationKey=..." \
|
|
17
|
+
--users=10 \
|
|
18
|
+
--days=1
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Parameters
|
|
22
|
+
|
|
23
|
+
| Parameter | Required | Default | Description |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| `--flag` | **Yes** | — | Feature flag name |
|
|
26
|
+
| `--feature-flag-reference` | **Yes** | — | Feature flag reference string |
|
|
27
|
+
| `--variants` | **Yes** | — | Comma-separated variant names (e.g. `control,treatment`) |
|
|
28
|
+
| `--connectionString` | **Yes** | — | Application Insights connection string |
|
|
29
|
+
| `--users` | No | `100` | Number of simulated users per hour |
|
|
30
|
+
| `--days` | No | `7` | Number of days to backfill |
|
|
31
|
+
| `--allocation-id` | No | `123e4567-e89b-12d3-a456-426655440000` | Allocation ID GUID |
|
|
32
|
+
|
|
33
|
+
## What Gets Generated
|
|
34
|
+
|
|
35
|
+
For each simulated user the tool emits:
|
|
36
|
+
|
|
37
|
+
1. **FeatureEvaluation event** — required by the experimentation pipeline, includes `FeatureName`, `Variant`, `TargetingId`, `FeatureFlagReference`, and `AllocationId`.
|
|
38
|
+
2. **Custom events** (~50 event types) — user interactions like `UserClickedButton`, `CompletedPurchase`, `SearchedContent`, etc., each with realistic measurements. Every event has a 90% chance of firing per user.
|
|
39
|
+
3. **Page views** (8 pages) — with browser timing measurements (`totalDuration`, `networkDuration`, etc.). Each page has a 70% chance of being visited per user.
|
|
40
|
+
4. **Exceptions** — simulated at a low rate (1% for the first variant, 3% for others).
|
|
41
|
+
|
|
42
|
+
Variant adjustments are applied to measurements so different variants produce statistically distinguishable data, letting you verify that the experimentation analysis pipeline detects meaningful differences.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/backfill.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const appInsights = require("applicationinsights");
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Constants
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
/** Flush to App Insights after this many tracked items */
|
|
12
|
+
const BATCH_SIZE = 500;
|
|
13
|
+
|
|
14
|
+
/** Pause (ms) after each flush to avoid throttling */
|
|
15
|
+
const FLUSH_DELAY_MS = 100;
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// Event definitions (ported from UserSimulationService.cs)
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
const EVENT_MEASUREMENTS = {
|
|
22
|
+
"UserClickedButton": { "time-to-click": 5000 },
|
|
23
|
+
"ReferredFriends": { "referred-friends": 2 },
|
|
24
|
+
"UserLoggedIn": { "login-duration-ms": 1200, "attempts": 1 },
|
|
25
|
+
"UserRegistered": { "time-to-complete-ms": 45000, "form-fields-filled": 8 },
|
|
26
|
+
"ClickedAccountDetails": { "page-load-time-ms": 800 },
|
|
27
|
+
"UpdatedProfile": { "time-spent-ms": 12000, "fields-updated": 3 },
|
|
28
|
+
"DeletedAccount": { "confirmation-time-ms": 8000 },
|
|
29
|
+
"ViewedDashboard": { "load-time-ms": 1500, "widgets-viewed": 5 },
|
|
30
|
+
"CreatedPost": { "composition-time-ms": 25000, "character-count": 150 },
|
|
31
|
+
"SharedContent": { "time-to-share-ms": 3000 },
|
|
32
|
+
"LikedPost": { "reaction-time-before-like-ms": 2000 },
|
|
33
|
+
"CommentedOnPost": { "time-to-comment-ms": 15000, "comment-length": 45 },
|
|
34
|
+
"FollowedUser": { "time-to-follow-ms": 1500 },
|
|
35
|
+
"UnfollowedUser": { "time-to-unfollow-ms": 800 },
|
|
36
|
+
"SearchedContent": { "search-time-ms": 500, "results-clicked": 1 },
|
|
37
|
+
"FilteredResults": { "filter-selection-time-ms": 2000 },
|
|
38
|
+
"SortedResults": { "sort-interaction-time-ms": 1000 },
|
|
39
|
+
"AddedToCart": { "time-to-add-ms": 2500, "quantity": 1 },
|
|
40
|
+
"RemovedFromCart": { "time-to-remove-ms": 1200 },
|
|
41
|
+
"ViewedCart": { "cart-load-time-ms": 900 },
|
|
42
|
+
"InitiatedCheckout": { "time-to-checkout-ms": 3500 },
|
|
43
|
+
"CompletedPurchase": { "checkout-duration-ms": 35000, "order-value": 75 },
|
|
44
|
+
"AppliedCoupon": { "time-to-apply-ms": 4000 },
|
|
45
|
+
"SelectedShippingMethod": { "selection-time-ms": 2800 },
|
|
46
|
+
"EnteredPaymentInfo": { "entry-time-ms": 18000 },
|
|
47
|
+
"SavedForLater": { "time-to-save-ms": 1800 },
|
|
48
|
+
"AddedToWishlist": { "time-to-wishlist-ms": 2200 },
|
|
49
|
+
"RemovedFromWishlist": { "time-to-remove-ms": 1100 },
|
|
50
|
+
"ViewedProductDetails": { "page-load-ms": 1100, "images-viewed": 3 },
|
|
51
|
+
"ZoomedProductImage": { "time-to-zoom-ms": 2500 },
|
|
52
|
+
"WatchedProductVideo": { "watch-duration-ms": 15000, "completion-percent": 60 },
|
|
53
|
+
"ReadReviews": { "time-reading-ms": 20000, "reviews-read": 4 },
|
|
54
|
+
"WroteReview": { "composition-time-ms": 40000, "review-length": 120, "star-rating": 4 },
|
|
55
|
+
"VotedOnReview": { "time-to-vote-ms": 1500 },
|
|
56
|
+
"AskedQuestion": { "time-to-ask-ms": 25000, "question-length": 80 },
|
|
57
|
+
"AnsweredQuestion": { "time-to-answer-ms": 30000, "answer-length": 100 },
|
|
58
|
+
"ClickedNotification": { "time-since-notification-ms": 12000 },
|
|
59
|
+
"DismissedNotification": { "time-to-dismiss-ms": 2000 },
|
|
60
|
+
"EnabledNotifications": { "time-to-enable-ms": 5000 },
|
|
61
|
+
"DisabledNotifications": { "time-to-disable-ms": 3000 },
|
|
62
|
+
"ChangedSettings": { "time-in-settings-ms": 15000, "settings-changed": 2 },
|
|
63
|
+
"UploadedAvatar": { "upload-time-ms": 8000, "file-size-kb": 250 },
|
|
64
|
+
"ConnectedSocialAccount": { "connection-time-ms": 12000 },
|
|
65
|
+
"DisconnectedSocialAccount": { "time-to-disconnect-ms": 4000 },
|
|
66
|
+
"InvitedFriend": { "invitation-time-ms": 6000 },
|
|
67
|
+
"AcceptedInvitation": { "time-to-accept-ms": 8000 },
|
|
68
|
+
"JoinedGroup": { "time-to-join-ms": 3500 },
|
|
69
|
+
"LeftGroup": { "time-to-leave-ms": 2500 },
|
|
70
|
+
"CreatedEvent": { "creation-time-ms": 35000, "attendees-invited": 10 },
|
|
71
|
+
"RSVPedToEvent": { "time-to-rsvp-ms": 4000 },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// Page views with baseline browser-timing measurements (ms)
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
const PAGE_VIEWS = [
|
|
79
|
+
{ name: "Home", url: "/", measurements: { "totalDuration": 1200, "networkDuration": 45, "sendDuration": 5, "receiveDuration": 35, "processingDuration": 800 } },
|
|
80
|
+
{ name: "Dashboard", url: "/dashboard", measurements: { "totalDuration": 1800, "networkDuration": 50, "sendDuration": 8, "receiveDuration": 45, "processingDuration": 1200 } },
|
|
81
|
+
{ name: "Profile", url: "/profile", measurements: { "totalDuration": 900, "networkDuration": 40, "sendDuration": 4, "receiveDuration": 30, "processingDuration": 600 } },
|
|
82
|
+
{ name: "Products", url: "/products", measurements: { "totalDuration": 2200, "networkDuration": 55, "sendDuration": 10, "receiveDuration": 60, "processingDuration": 1500 } },
|
|
83
|
+
{ name: "Cart", url: "/cart", measurements: { "totalDuration": 1100, "networkDuration": 42, "sendDuration": 6, "receiveDuration": 38, "processingDuration": 700 } },
|
|
84
|
+
{ name: "Checkout", url: "/checkout", measurements: { "totalDuration": 1500, "networkDuration": 48, "sendDuration": 7, "receiveDuration": 42, "processingDuration": 1000 } },
|
|
85
|
+
{ name: "Search Results", url: "/search", measurements: { "totalDuration": 1400, "networkDuration": 44, "sendDuration": 6, "receiveDuration": 40, "processingDuration": 950 } },
|
|
86
|
+
{ name: "Settings", url: "/settings", measurements: { "totalDuration": 800, "networkDuration": 38, "sendDuration": 4, "receiveDuration": 28, "processingDuration": 550 } },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// Variant adjustments (ported from VariantAdjustments.cs)
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
const VARIANT_ADJUSTMENTS = [
|
|
94
|
+
{
|
|
95
|
+
"time-to-click": -50, "referred-friends": 0, "login-duration-ms": 0,
|
|
96
|
+
"attempts": 0, "time-to-complete-ms": 0, "form-fields-filled": 0,
|
|
97
|
+
"page-load-time-ms": 0, "time-spent-ms": 0, "confirmation-time-ms": 0,
|
|
98
|
+
"load-time-ms": 0, "widgets-viewed": 0, "composition-time-ms": 0,
|
|
99
|
+
"character-count": 0, "time-to-share-ms": 0, "reaction-time-before-like-ms": 0,
|
|
100
|
+
"time-to-comment-ms": 0, "comment-length": 0, "time-to-follow-ms": 0,
|
|
101
|
+
"time-to-unfollow-ms": 0, "search-time-ms": 0, "results-clicked": 0,
|
|
102
|
+
"filter-selection-time-ms": 0, "sort-interaction-time-ms": 0,
|
|
103
|
+
"time-to-add-ms": 0, "quantity": 0, "time-to-remove-ms": 0,
|
|
104
|
+
"cart-load-time-ms": 0, "time-to-checkout-ms": 0, "checkout-duration-ms": 0,
|
|
105
|
+
"order-value": 0, "time-to-apply-ms": 0, "selection-time-ms": 0,
|
|
106
|
+
"entry-time-ms": 0, "time-to-save-ms": 0, "time-to-wishlist-ms": 0,
|
|
107
|
+
"images-viewed": 0, "time-to-zoom-ms": 0, "watch-duration-ms": 0,
|
|
108
|
+
"completion-percent": 0, "time-reading-ms": 0, "reviews-read": 0,
|
|
109
|
+
"review-length": 0, "star-rating": 0, "time-to-vote-ms": 0,
|
|
110
|
+
"time-to-ask-ms": 0, "question-length": 0, "time-to-answer-ms": 0,
|
|
111
|
+
"answer-length": 0, "time-since-notification-ms": 0, "time-to-dismiss-ms": 0,
|
|
112
|
+
"time-to-enable-ms": 0, "time-to-disable-ms": 0, "time-in-settings-ms": 0,
|
|
113
|
+
"settings-changed": 0, "upload-time-ms": 0, "file-size-kb": 0,
|
|
114
|
+
"connection-time-ms": 0, "time-to-disconnect-ms": 0,
|
|
115
|
+
"invitation-time-ms": 0, "time-to-accept-ms": 0, "time-to-join-ms": 0,
|
|
116
|
+
"time-to-leave-ms": 0, "creation-time-ms": 0, "attendees-invited": 0,
|
|
117
|
+
"time-to-rsvp-ms": 0,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"time-to-click": -800, "referred-friends": 1, "login-duration-ms": -100,
|
|
121
|
+
"attempts": 0, "time-to-complete-ms": -3000, "form-fields-filled": 0,
|
|
122
|
+
"page-load-time-ms": -50, "time-spent-ms": -500, "confirmation-time-ms": -500,
|
|
123
|
+
"load-time-ms": -100, "widgets-viewed": 1, "composition-time-ms": -2000,
|
|
124
|
+
"character-count": 10, "time-to-share-ms": -300, "reaction-time-before-like-ms": -200,
|
|
125
|
+
"time-to-comment-ms": -1000, "comment-length": 5, "time-to-follow-ms": -100,
|
|
126
|
+
"time-to-unfollow-ms": -50, "search-time-ms": -50, "results-clicked": 0.2,
|
|
127
|
+
"filter-selection-time-ms": -200, "sort-interaction-time-ms": -100,
|
|
128
|
+
"time-to-add-ms": -200, "quantity": 0.2, "time-to-remove-ms": -100,
|
|
129
|
+
"cart-load-time-ms": -80, "time-to-checkout-ms": -300, "checkout-duration-ms": -3000,
|
|
130
|
+
"order-value": 10, "time-to-apply-ms": -400, "selection-time-ms": -200,
|
|
131
|
+
"entry-time-ms": -1500, "time-to-save-ms": -150, "time-to-wishlist-ms": -200,
|
|
132
|
+
"images-viewed": 0.5, "time-to-zoom-ms": -200, "watch-duration-ms": 2000,
|
|
133
|
+
"completion-percent": 8, "time-reading-ms": 2000, "reviews-read": 1,
|
|
134
|
+
"review-length": 15, "star-rating": 0.2, "time-to-vote-ms": -150,
|
|
135
|
+
"time-to-ask-ms": -2000, "question-length": 8, "time-to-answer-ms": -2500,
|
|
136
|
+
"answer-length": 12, "time-since-notification-ms": -1000, "time-to-dismiss-ms": -200,
|
|
137
|
+
"time-to-enable-ms": -500, "time-to-disable-ms": -300, "time-in-settings-ms": -1000,
|
|
138
|
+
"settings-changed": 0.3, "upload-time-ms": -800, "file-size-kb": 20,
|
|
139
|
+
"connection-time-ms": -1200, "time-to-disconnect-ms": -400,
|
|
140
|
+
"invitation-time-ms": -600, "time-to-accept-ms": -800, "time-to-join-ms": -300,
|
|
141
|
+
"time-to-leave-ms": -200, "creation-time-ms": -3000, "attendees-invited": 2,
|
|
142
|
+
"time-to-rsvp-ms": -400,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"time-to-click": -1500, "referred-friends": 3, "login-duration-ms": -300,
|
|
146
|
+
"attempts": -0.3, "time-to-complete-ms": -8000, "form-fields-filled": 1,
|
|
147
|
+
"page-load-time-ms": -200, "time-spent-ms": -2000, "confirmation-time-ms": -1500,
|
|
148
|
+
"load-time-ms": -400, "widgets-viewed": 2, "composition-time-ms": -5000,
|
|
149
|
+
"character-count": 30, "time-to-share-ms": -800, "reaction-time-before-like-ms": -500,
|
|
150
|
+
"time-to-comment-ms": -3000, "comment-length": 15, "time-to-follow-ms": -400,
|
|
151
|
+
"time-to-unfollow-ms": -200, "search-time-ms": -150, "results-clicked": 0.5,
|
|
152
|
+
"filter-selection-time-ms": -500, "sort-interaction-time-ms": -300,
|
|
153
|
+
"time-to-add-ms": -600, "quantity": 0.5, "time-to-remove-ms": -300,
|
|
154
|
+
"cart-load-time-ms": -250, "time-to-checkout-ms": -800, "checkout-duration-ms": -8000,
|
|
155
|
+
"order-value": 25, "time-to-apply-ms": -1000, "selection-time-ms": -600,
|
|
156
|
+
"entry-time-ms": -4000, "time-to-save-ms": -400, "time-to-wishlist-ms": -500,
|
|
157
|
+
"images-viewed": 1, "time-to-zoom-ms": -600, "watch-duration-ms": 5000,
|
|
158
|
+
"completion-percent": 20, "time-reading-ms": 5000, "reviews-read": 2,
|
|
159
|
+
"review-length": 40, "star-rating": 0.5, "time-to-vote-ms": -400,
|
|
160
|
+
"time-to-ask-ms": -5000, "question-length": 20, "time-to-answer-ms": -6000,
|
|
161
|
+
"answer-length": 30, "time-since-notification-ms": -3000, "time-to-dismiss-ms": -500,
|
|
162
|
+
"time-to-enable-ms": -1200, "time-to-disable-ms": -800, "time-in-settings-ms": -3000,
|
|
163
|
+
"settings-changed": 1, "upload-time-ms": -2000, "file-size-kb": 50,
|
|
164
|
+
"connection-time-ms": -3000, "time-to-disconnect-ms": -1000,
|
|
165
|
+
"invitation-time-ms": -1500, "time-to-accept-ms": -2000, "time-to-join-ms": -800,
|
|
166
|
+
"time-to-leave-ms": -600, "creation-time-ms": -8000, "attendees-invited": 5,
|
|
167
|
+
"time-to-rsvp-ms": -1000,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"time-to-click": -1000, "referred-friends": 2, "login-duration-ms": -200,
|
|
171
|
+
"attempts": -0.2, "time-to-complete-ms": -6000, "form-fields-filled": 0.5,
|
|
172
|
+
"page-load-time-ms": -150, "time-spent-ms": -1500, "confirmation-time-ms": -1000,
|
|
173
|
+
"load-time-ms": -300, "widgets-viewed": 1.5, "composition-time-ms": -4000,
|
|
174
|
+
"character-count": 20, "time-to-share-ms": -600, "reaction-time-before-like-ms": -400,
|
|
175
|
+
"time-to-comment-ms": -2500, "comment-length": 10, "time-to-follow-ms": -300,
|
|
176
|
+
"time-to-unfollow-ms": 100, "search-time-ms": -120, "results-clicked": 0.4,
|
|
177
|
+
"filter-selection-time-ms": -400, "sort-interaction-time-ms": -250,
|
|
178
|
+
"time-to-add-ms": -500, "quantity": 0.4, "time-to-remove-ms": -250,
|
|
179
|
+
"cart-load-time-ms": -200, "time-to-checkout-ms": -650, "checkout-duration-ms": -6500,
|
|
180
|
+
"order-value": 18, "time-to-apply-ms": -800, "selection-time-ms": -500,
|
|
181
|
+
"entry-time-ms": -3200, "time-to-save-ms": -300, "time-to-wishlist-ms": -400,
|
|
182
|
+
"images-viewed": 0.8, "time-to-zoom-ms": -500, "watch-duration-ms": 4000,
|
|
183
|
+
"completion-percent": 15, "time-reading-ms": 4000, "reviews-read": 1.5,
|
|
184
|
+
"review-length": 30, "star-rating": 0.3, "time-to-vote-ms": -300,
|
|
185
|
+
"time-to-ask-ms": -4000, "question-length": 15, "time-to-answer-ms": -5000,
|
|
186
|
+
"answer-length": 25, "time-since-notification-ms": -2500, "time-to-dismiss-ms": -400,
|
|
187
|
+
"time-to-enable-ms": -1000, "time-to-disable-ms": -600, "time-in-settings-ms": 500,
|
|
188
|
+
"settings-changed": 0.8, "upload-time-ms": -1600, "file-size-kb": 40,
|
|
189
|
+
"connection-time-ms": -2500, "time-to-disconnect-ms": -800,
|
|
190
|
+
"invitation-time-ms": -1200, "time-to-accept-ms": -1600, "time-to-join-ms": -600,
|
|
191
|
+
"time-to-leave-ms": -500, "creation-time-ms": -6500, "attendees-invited": 4,
|
|
192
|
+
"time-to-rsvp-ms": -800,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"time-to-click": -1800, "referred-friends": 3.5, "login-duration-ms": -350,
|
|
196
|
+
"attempts": -0.4, "time-to-complete-ms": -9000, "form-fields-filled": 1.2,
|
|
197
|
+
"page-load-time-ms": -250, "time-spent-ms": -2500, "confirmation-time-ms": -1800,
|
|
198
|
+
"load-time-ms": -500, "widgets-viewed": 2.5, "composition-time-ms": -6000,
|
|
199
|
+
"character-count": 40, "time-to-share-ms": -900, "reaction-time-before-like-ms": -600,
|
|
200
|
+
"time-to-comment-ms": -3500, "comment-length": 20, "time-to-follow-ms": -500,
|
|
201
|
+
"time-to-unfollow-ms": -250, "search-time-ms": -180, "results-clicked": 0.6,
|
|
202
|
+
"filter-selection-time-ms": -600, "sort-interaction-time-ms": -350,
|
|
203
|
+
"time-to-add-ms": -700, "quantity": 0.6, "time-to-remove-ms": -350,
|
|
204
|
+
"cart-load-time-ms": -300, "time-to-checkout-ms": -900, "checkout-duration-ms": -9000,
|
|
205
|
+
"order-value": 30, "time-to-apply-ms": -1200, "selection-time-ms": -700,
|
|
206
|
+
"entry-time-ms": -4500, "time-to-save-ms": -500, "time-to-wishlist-ms": -600,
|
|
207
|
+
"images-viewed": 1.2, "time-to-zoom-ms": -700, "watch-duration-ms": 6000,
|
|
208
|
+
"completion-percent": 25, "time-reading-ms": 6000, "reviews-read": 2.5,
|
|
209
|
+
"review-length": 50, "star-rating": 0.6, "time-to-vote-ms": -500,
|
|
210
|
+
"time-to-ask-ms": -6000, "question-length": 25, "time-to-answer-ms": -7000,
|
|
211
|
+
"answer-length": 35, "time-since-notification-ms": -3500, "time-to-dismiss-ms": -600,
|
|
212
|
+
"time-to-enable-ms": -1400, "time-to-disable-ms": 200, "time-in-settings-ms": -3500,
|
|
213
|
+
"settings-changed": 1.2, "upload-time-ms": -2500, "file-size-kb": 60,
|
|
214
|
+
"connection-time-ms": -3500, "time-to-disconnect-ms": -1200,
|
|
215
|
+
"invitation-time-ms": -1800, "time-to-accept-ms": 500, "time-to-join-ms": -900,
|
|
216
|
+
"time-to-leave-ms": -700, "creation-time-ms": -9000, "attendees-invited": 6,
|
|
217
|
+
"time-to-rsvp-ms": -1200,
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// Browser-timing variant adjustments (ms deltas applied to PAGE_VIEWS baselines)
|
|
222
|
+
const BROWSER_TIMING_VARIANT_ADJUSTMENTS = [
|
|
223
|
+
// Variant 0 (control): no change
|
|
224
|
+
{ "totalDuration": 0, "networkDuration": 0, "sendDuration": 0, "receiveDuration": 0, "processingDuration": 0 },
|
|
225
|
+
// Variant 1: slight improvement
|
|
226
|
+
{ "totalDuration": -100, "networkDuration": -5, "sendDuration": -0.5, "receiveDuration": -3, "processingDuration": -60 },
|
|
227
|
+
// Variant 2: significant improvement
|
|
228
|
+
{ "totalDuration": -300, "networkDuration": -12, "sendDuration": -1.5, "receiveDuration": -8, "processingDuration": -180 },
|
|
229
|
+
// Variant 3: moderate improvement
|
|
230
|
+
{ "totalDuration": -200, "networkDuration": -8, "sendDuration": -1, "receiveDuration": -6, "processingDuration": -130 },
|
|
231
|
+
// Variant 4: best improvement
|
|
232
|
+
{ "totalDuration": -400, "networkDuration": -15, "sendDuration": -2, "receiveDuration": -10, "processingDuration": -220 },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// CLI argument parsing
|
|
237
|
+
// ============================================================
|
|
238
|
+
|
|
239
|
+
function parseArgs() {
|
|
240
|
+
const args = {};
|
|
241
|
+
for (const arg of process.argv.slice(2)) {
|
|
242
|
+
const match = arg.match(/^--([a-zA-Z][\w-]*)=(.+)$/);
|
|
243
|
+
if (match) args[match[1]] = match[2];
|
|
244
|
+
}
|
|
245
|
+
return args;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// Helpers
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
function generateUserId() {
|
|
253
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Deterministic variant assignment based on userId hash */
|
|
257
|
+
function assignVariant(userId, variants) {
|
|
258
|
+
const hash = crypto.createHash("md5").update(userId).digest();
|
|
259
|
+
const index = hash.readUInt32BE(0) % variants.length;
|
|
260
|
+
return variants[index];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Compute adjusted measurements for an event, matching the C# LogEvents logic */
|
|
264
|
+
function computeMeasurements(baselineMeasurements, adjustments) {
|
|
265
|
+
const measurements = {};
|
|
266
|
+
|
|
267
|
+
for (const [name, baseValue] of Object.entries(baselineMeasurements)) {
|
|
268
|
+
let adjusted = baseValue;
|
|
269
|
+
|
|
270
|
+
if (adjustments[name] !== undefined) {
|
|
271
|
+
// Random factor between 0.5 and 1.5 (matches C# logic)
|
|
272
|
+
const randomFactor = 0.5 + Math.random();
|
|
273
|
+
adjusted += adjustments[name] * randomFactor;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Clamp values (matches C# logic)
|
|
277
|
+
if (name === "star-rating") {
|
|
278
|
+
adjusted = Math.max(1, Math.min(5, adjusted));
|
|
279
|
+
} else if (name === "attempts") {
|
|
280
|
+
adjusted = Math.max(1, adjusted);
|
|
281
|
+
} else {
|
|
282
|
+
adjusted = Math.max(0, adjusted);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
measurements[name] = Math.round(adjusted * 100) / 100;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return measurements;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Flush the App Insights client and wait briefly */
|
|
292
|
+
function flushClient(client) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
client.flush({
|
|
295
|
+
callback: () => setTimeout(resolve, FLUSH_DELAY_MS),
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatNumber(n) {
|
|
301
|
+
return n.toLocaleString();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============================================================
|
|
305
|
+
// Main
|
|
306
|
+
// ============================================================
|
|
307
|
+
|
|
308
|
+
async function main() {
|
|
309
|
+
const args = parseArgs();
|
|
310
|
+
|
|
311
|
+
// --- Validate arguments ---
|
|
312
|
+
const flagName = args.flag;
|
|
313
|
+
const featureFlagReference = args["feature-flag-reference"];
|
|
314
|
+
const variantsCsv = args.variants;
|
|
315
|
+
const connectionString = args.connectionString;
|
|
316
|
+
if (!flagName || !featureFlagReference || !variantsCsv || !connectionString) {
|
|
317
|
+
console.error(
|
|
318
|
+
"Usage: node backfill.js --flag=<name> --feature-flag-reference=<ref> --variants=A,B --connectionString=<connStr> [--users=100] [--days=7] [--allocation-id=<guid>]"
|
|
319
|
+
);
|
|
320
|
+
console.error("\nRequired:");
|
|
321
|
+
console.error(" --flag Feature flag name");
|
|
322
|
+
console.error(" --feature-flag-reference Feature flag reference string");
|
|
323
|
+
console.error(" --variants Comma-separated variant names (e.g. control,treatment)");
|
|
324
|
+
console.error(" --connectionString App Insights connection string");
|
|
325
|
+
console.error("\nOptional:");
|
|
326
|
+
console.error(" --users Users per hour (default: 100)");
|
|
327
|
+
console.error(" --days Days to backfill (default: 7)");
|
|
328
|
+
console.error(" --allocation-id Allocation ID GUID (default: 123e4567-e89b-12d3-a456-426655440000)");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const allocationId = args["allocation-id"] || "123e4567-e89b-12d3-a456-426655440000";
|
|
333
|
+
|
|
334
|
+
const usersPerHour = parseInt(args.users || "100", 10);
|
|
335
|
+
const days = parseInt(args.days || "7", 10);
|
|
336
|
+
|
|
337
|
+
// --- Resolve variants ---
|
|
338
|
+
const variantList = variantsCsv.split(",").map((v) => v.trim()).filter(Boolean);
|
|
339
|
+
|
|
340
|
+
if (variantList.length === 0) {
|
|
341
|
+
console.error("--variants must contain at least one variant name.");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (variantList.length > VARIANT_ADJUSTMENTS.length) {
|
|
346
|
+
console.error(
|
|
347
|
+
`Too many variants (${variantList.length}). Only ${VARIANT_ADJUSTMENTS.length} adjustment profiles are available.`
|
|
348
|
+
);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build a map from variant name -> adjustment profile (by index)
|
|
353
|
+
const variantAdjustmentMap = {};
|
|
354
|
+
const browserTimingAdjustmentMap = {};
|
|
355
|
+
variantList.forEach((name, i) => {
|
|
356
|
+
variantAdjustmentMap[name] = VARIANT_ADJUSTMENTS[i];
|
|
357
|
+
browserTimingAdjustmentMap[name] = BROWSER_TIMING_VARIANT_ADJUSTMENTS[i];
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// --- Setup App Insights (disable all auto-collection) ---
|
|
361
|
+
appInsights.setup(connectionString)
|
|
362
|
+
.setAutoCollectConsole(false, false)
|
|
363
|
+
.setAutoCollectDependencies(false)
|
|
364
|
+
.setAutoCollectExceptions(false)
|
|
365
|
+
.setAutoCollectPerformance(false, false)
|
|
366
|
+
.setAutoCollectPreAggregatedMetrics(false)
|
|
367
|
+
.setAutoCollectRequests(false)
|
|
368
|
+
.setAutoCollectHeartbeat(false)
|
|
369
|
+
.setAutoDependencyCorrelation(false)
|
|
370
|
+
.setUseDiskRetryCaching(false)
|
|
371
|
+
.start();
|
|
372
|
+
|
|
373
|
+
const client = appInsights.defaultClient;
|
|
374
|
+
client.config.maxBatchSize = 250;
|
|
375
|
+
|
|
376
|
+
// --- Compute totals for progress display ---
|
|
377
|
+
const eventCount = Object.keys(EVENT_MEASUREMENTS).length;
|
|
378
|
+
const pageViewCount = PAGE_VIEWS.length;
|
|
379
|
+
const avgEventsPerUser = Math.round(eventCount * 0.9); // 90% emit rate
|
|
380
|
+
const avgPageViewsPerUser = Math.round(pageViewCount * 0.7); // 70% visit rate
|
|
381
|
+
const totalUsers = usersPerHour * 24 * days;
|
|
382
|
+
const estTotalEvents = totalUsers * (1 + avgEventsPerUser + avgPageViewsPerUser); // +1 for FeatureEvaluation
|
|
383
|
+
|
|
384
|
+
console.log();
|
|
385
|
+
console.log("=== Experimentation Backfill ===");
|
|
386
|
+
console.log(` Flag: ${flagName}`);
|
|
387
|
+
console.log(` Variants: ${variantList.join(", ")}`);
|
|
388
|
+
console.log(` Users/hour: ${formatNumber(usersPerHour)}`);
|
|
389
|
+
console.log(` Days: ${days}`);
|
|
390
|
+
console.log(` Total users: ${formatNumber(totalUsers)}`);
|
|
391
|
+
console.log(` Events/user: ~${avgEventsPerUser} (of ${eventCount} possible, 90% chance each)`);
|
|
392
|
+
console.log(` PageViews/user: ~${avgPageViewsPerUser} (of ${pageViewCount} pages, 70% chance each)`);
|
|
393
|
+
console.log(` Est. total: ~${formatNumber(estTotalEvents)} telemetry items`);
|
|
394
|
+
console.log(` Batch size: ${BATCH_SIZE} (flush threshold)`);
|
|
395
|
+
console.log();
|
|
396
|
+
|
|
397
|
+
const now = new Date();
|
|
398
|
+
let totalTracked = 0;
|
|
399
|
+
let pendingCount = 0;
|
|
400
|
+
const startTime = Date.now();
|
|
401
|
+
|
|
402
|
+
// --- Iterate: days -> hours -> users ---
|
|
403
|
+
for (let dayOffset = days; dayOffset >= 1; dayOffset--) {
|
|
404
|
+
const dayStart = new Date(now);
|
|
405
|
+
dayStart.setDate(dayStart.getDate() - dayOffset);
|
|
406
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
407
|
+
|
|
408
|
+
const dayLabel = dayStart.toISOString().slice(0, 10);
|
|
409
|
+
console.log(`Day ${dayLabel} (${dayOffset}d ago)`);
|
|
410
|
+
|
|
411
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
412
|
+
const hourStart = new Date(dayStart);
|
|
413
|
+
hourStart.setHours(hour);
|
|
414
|
+
|
|
415
|
+
// Spread users evenly across the hour
|
|
416
|
+
const intervalMs = (3600 * 1000) / usersPerHour;
|
|
417
|
+
|
|
418
|
+
for (let u = 0; u < usersPerHour; u++) {
|
|
419
|
+
const userId = generateUserId();
|
|
420
|
+
const variant = assignVariant(userId, variantList);
|
|
421
|
+
const userStartMs = hourStart.getTime() + Math.floor(u * intervalMs);
|
|
422
|
+
const sessionId = crypto.randomUUID();
|
|
423
|
+
|
|
424
|
+
// --- FeatureEvaluation event (needed for experimentation pipeline) ---
|
|
425
|
+
client.trackEvent({
|
|
426
|
+
name: "FeatureEvaluation",
|
|
427
|
+
time: new Date(userStartMs),
|
|
428
|
+
properties: {
|
|
429
|
+
FeatureName: flagName,
|
|
430
|
+
Enabled: "True",
|
|
431
|
+
Variant: variant,
|
|
432
|
+
DefaultWhenEnabled: variantList[0],
|
|
433
|
+
VariantAssignmentReason: "Percentile",
|
|
434
|
+
TargetingId: userId,
|
|
435
|
+
FeatureFlagReference: featureFlagReference,
|
|
436
|
+
AllocationId: allocationId,
|
|
437
|
+
},
|
|
438
|
+
tagOverrides: {
|
|
439
|
+
"ai.user.id": userId,
|
|
440
|
+
"ai.session.id": sessionId,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
pendingCount++;
|
|
444
|
+
totalTracked++;
|
|
445
|
+
|
|
446
|
+
// --- Custom events (1 second apart) ---
|
|
447
|
+
let eventOffsetMs = 1000;
|
|
448
|
+
for (const [eventName, baselineMeasurements] of Object.entries(EVENT_MEASUREMENTS)) {
|
|
449
|
+
// 90% chance to emit each event (matches C# logic)
|
|
450
|
+
if (Math.random() >= 0.9) continue;
|
|
451
|
+
|
|
452
|
+
const measurements = computeMeasurements(baselineMeasurements, variantAdjustmentMap[variant] || {});
|
|
453
|
+
const eventTime = new Date(userStartMs + eventOffsetMs);
|
|
454
|
+
|
|
455
|
+
client.trackEvent({
|
|
456
|
+
name: eventName,
|
|
457
|
+
time: eventTime,
|
|
458
|
+
properties: { TargetingId: userId },
|
|
459
|
+
measurements,
|
|
460
|
+
tagOverrides: {
|
|
461
|
+
"ai.user.id": userId,
|
|
462
|
+
"ai.session.id": sessionId,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
pendingCount++;
|
|
467
|
+
totalTracked++;
|
|
468
|
+
eventOffsetMs += 1000;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// --- Browser timings / page views (500ms apart) ---
|
|
472
|
+
for (const page of PAGE_VIEWS) {
|
|
473
|
+
// 70% chance to visit each page
|
|
474
|
+
if (Math.random() >= 0.7) continue;
|
|
475
|
+
|
|
476
|
+
const timings = computeMeasurements(page.measurements, browserTimingAdjustmentMap[variant] || {});
|
|
477
|
+
const pageTime = new Date(userStartMs + eventOffsetMs);
|
|
478
|
+
|
|
479
|
+
client.trackPageView({
|
|
480
|
+
name: page.name,
|
|
481
|
+
url: page.url,
|
|
482
|
+
duration: timings.totalDuration,
|
|
483
|
+
time: pageTime,
|
|
484
|
+
properties: { TargetingId: userId },
|
|
485
|
+
measurements: timings,
|
|
486
|
+
tagOverrides: {
|
|
487
|
+
"ai.user.id": userId,
|
|
488
|
+
"ai.session.id": sessionId,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
pendingCount++;
|
|
493
|
+
totalTracked++;
|
|
494
|
+
eventOffsetMs += 500;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// --- Exception simulation ---
|
|
498
|
+
// First variant gets a lower exception rate; others get the higher rate
|
|
499
|
+
const exceptionChance = variant === variantList[0] ? 0.01 : 0.03;
|
|
500
|
+
if (Math.random() < exceptionChance) {
|
|
501
|
+
client.trackException({
|
|
502
|
+
exception: new Error(`Simulated user exception for variant: ${variant}`),
|
|
503
|
+
time: new Date(userStartMs + eventOffsetMs),
|
|
504
|
+
properties: { TargetingId: userId },
|
|
505
|
+
tagOverrides: {
|
|
506
|
+
"ai.user.id": userId,
|
|
507
|
+
"ai.session.id": sessionId,
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
pendingCount++;
|
|
511
|
+
totalTracked++;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- Flush when batch threshold reached ---
|
|
515
|
+
if (pendingCount >= BATCH_SIZE) {
|
|
516
|
+
await flushClient(client);
|
|
517
|
+
pendingCount = 0;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Progress update per hour
|
|
522
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
523
|
+
const rate = Math.round(totalTracked / elapsed);
|
|
524
|
+
const pct = ((totalTracked / estTotalEvents) * 100).toFixed(1);
|
|
525
|
+
process.stdout.write(
|
|
526
|
+
`\r Hour ${hour.toString().padStart(2, "0")} | ${formatNumber(totalTracked)} events (${pct}%) | ${formatNumber(rate)} events/sec`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log(); // newline after each day
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- Final flush ---
|
|
534
|
+
console.log("\nFlushing remaining telemetry...");
|
|
535
|
+
await flushClient(client);
|
|
536
|
+
|
|
537
|
+
const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
538
|
+
console.log(`\nDone! Sent ${formatNumber(totalTracked)} events in ${totalElapsed}s`);
|
|
539
|
+
|
|
540
|
+
// Give the SDK a moment to finish any in-flight sends
|
|
541
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
main().catch((err) => {
|
|
546
|
+
console.error("\nFatal error:", err);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "appconfig-experiment-backfill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Generate mock Application Insights telemetry for testing Azure App Configuration Experimentation",
|
|
5
|
+
"main": "backfill.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"appconfig-experiment-backfill": "backfill.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"backfill.js"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node backfill.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"azure-app-configuration",
|
|
17
|
+
"experimentation",
|
|
18
|
+
"feature-flags",
|
|
19
|
+
"application-insights",
|
|
20
|
+
"mock-data",
|
|
21
|
+
"telemetry-backfill"
|
|
22
|
+
],
|
|
23
|
+
"author": "rossgrambo",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"applicationinsights": "^2.9.0"
|
|
27
|
+
}
|
|
28
|
+
}
|