Skip to content

Commit 702b343

Browse files
author
Mateus Medeiros
committed
Reduce App Insights exception noise
1 parent fce05ca commit 702b343

14 files changed

Lines changed: 349 additions & 50 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
name: writefluency-deploy
3+
description: WriteFluency production deployment workflow for committing staged changes, pushing master, creating service version tags, and pushing those tags to trigger GitHub Actions deploys. Use when the user asks to deploy, push to production/prd, trigger production deploys, release staged changes, or create/push deploy tags for WriteFluency services.
4+
---
5+
6+
# WriteFluency Deploy
7+
8+
## Workflow
9+
10+
Use this workflow for production deploys from `/Users/mateusmedeiros/Desktop/repos/WriteFluency`.
11+
12+
1. Inspect the staged set and branch:
13+
14+
```bash
15+
git status --short --branch
16+
git diff --cached --stat
17+
git diff --cached --name-only
18+
```
19+
20+
Commit only staged changes unless the user explicitly asks to stage more. If there are no staged changes, stop and tell the user.
21+
22+
2. Identify deploy targets from staged paths:
23+
24+
- `src/propositions-service/**` or `tests/propositions-service/**` -> `propositions-v*`
25+
- `src/webapp/**` -> `webapp-v*`
26+
- `src/users-service/**` or `tests/users-service/**` -> `users-v*`
27+
- `src/users-progress-service/**`, `tests/users-progress-service/**`, or `infra/users-progress/**` -> check workflows before tagging; current workflows use `users-v*` for `deploy-users-progress.yml`, despite historical `users-progress-v*` tags.
28+
- `.github/workflows/deploy-*.yml`, `src/host/WriteFluency.AppHost/deploy-k8s.sh`, or `src/host/WriteFluency.AppHost/aspirate-overlays-*` -> inspect workflows and ask if service tags are not obvious.
29+
30+
3. Verify tag triggers before creating tags:
31+
32+
```bash
33+
rg -n "tags:|propositions-v|webapp-v|users-v|users-progress-v" .github/workflows
34+
git tag --sort=-creatordate | head -n 30
35+
```
36+
37+
Choose the next patch version for each affected service. Examples:
38+
39+
- latest `propositions-v1.0.8` -> next `propositions-v1.0.9`
40+
- latest `webapp-v1.0.9` -> next `webapp-v1.0.10`
41+
- latest `users-v1.1.16` -> next `users-v1.1.17`
42+
43+
4. Commit staged changes with a concise message:
44+
45+
```bash
46+
git commit -m "<message>"
47+
```
48+
49+
5. Push `master` before tags:
50+
51+
```bash
52+
git push origin master
53+
```
54+
55+
6. Create tags on the pushed commit and push them:
56+
57+
```bash
58+
git tag <service-vX.Y.Z> HEAD
59+
git push origin <service-vX.Y.Z> [<another-service-vX.Y.Z>...]
60+
```
61+
62+
7. Confirm final state:
63+
64+
```bash
65+
git status --short --branch
66+
git log --oneline -n 3 --decorate
67+
git ls-remote --tags origin <tag-1> <tag-2>
68+
```
69+
70+
## Guardrails
71+
72+
- Do not rewrite, delete, or move existing tags.
73+
- Do not run `git reset`, `git checkout --`, or otherwise discard work.
74+
- Do not commit unstaged changes unless the user explicitly asks.
75+
- If staged changes span multiple services, create one tag per affected service after pushing the commit.
76+
- If the branch is not `master`, ask before proceeding unless the user explicitly requested the current branch.
77+
- If `origin/master` has new commits, pull/rebase only after telling the user what will happen.
78+
- If tests were not already run in the session, run focused tests when the staged changes include code, unless the user asked for a fast deploy.
79+
80+
## Known Tags
81+
82+
As of the current repo workflow, production deploys are triggered by:
83+
84+
- `.github/workflows/deploy-propositions.yml`: `propositions-v*`
85+
- `.github/workflows/deploy-webapp.yml`: `webapp-v*`
86+
- `.github/workflows/deploy-users.yml`: `users-v*`
87+
- `.github/workflows/deploy-users-progress.yml`: `users-v*`
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
interface:
2+
display_name: "WriteFluency Deploy"
3+
short_description: "Commit and tag WriteFluency deploys."
4+
default_prompt: "Use $writefluency-deploy to commit staged WriteFluency changes, push master, and create the correct production deploy tags."
5+
6+
policy:
7+
allow_implicit_invocation: true

infra/users-progress/main.bicep

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ param cosmosAccountName string
8383
@description('Cosmos DB endpoint URL.')
8484
param cosmosEndpoint string
8585

86+
@description('Cosmos DB preferred regions for the East US Function App, in priority order.')
87+
param eastUsCosmosPreferredRegions array = [
88+
'East US'
89+
'Southeast Asia'
90+
]
91+
92+
@description('Cosmos DB preferred regions for the Southeast Asia Function App, in priority order.')
93+
param southeastAsiaCosmosPreferredRegions array = [
94+
'Southeast Asia'
95+
'East US'
96+
]
97+
8698
@description('Cosmos database name.')
8799
param cosmosDatabaseName string = 'wf-users-progress'
88100

@@ -399,6 +411,16 @@ var corsAppSettings = [for (origin, index) in corsAllowedOrigins: {
399411
value: origin
400412
}]
401413

414+
var eastUsCosmosPreferredRegionAppSettings = [for (region, index) in eastUsCosmosPreferredRegions: {
415+
name: 'Cosmos__PreferredRegions__${index}'
416+
value: region
417+
}]
418+
419+
var southeastAsiaCosmosPreferredRegionAppSettings = [for (region, index) in southeastAsiaCosmosPreferredRegions: {
420+
name: 'Cosmos__PreferredRegions__${index}'
421+
value: region
422+
}]
423+
402424
var telemetryTuningAppSettings = [
403425
{
404426
name: 'AzureFunctionsJobHost__logging__logLevel__default'
@@ -493,14 +515,18 @@ resource functionAppEastUs 'Microsoft.Web/sites@2024-04-01' = {
493515
maximumInstanceCount: maximumInstanceCount
494516
instanceMemoryMB: instanceMemoryMB
495517
}
518+
#disable-next-line BCP037
519+
siteUpdateStrategy: {
520+
type: 'RollingUpdate'
521+
}
496522
}
497523
siteConfig: {
498524
vnetRouteAllEnabled: true
499525
cors: {
500526
allowedOrigins: corsAllowedOrigins
501527
supportCredentials: true
502528
}
503-
appSettings: concat(appSettingsBase, corsAppSettings, telemetryTuningAppSettings, [
529+
appSettings: concat(appSettingsBase, eastUsCosmosPreferredRegionAppSettings, corsAppSettings, telemetryTuningAppSettings, [
504530
{
505531
name: 'AzureWebJobsStorage__accountName'
506532
value: storageEastUs.name
@@ -553,14 +579,18 @@ resource functionAppSoutheastAsia 'Microsoft.Web/sites@2024-04-01' = {
553579
maximumInstanceCount: maximumInstanceCount
554580
instanceMemoryMB: instanceMemoryMB
555581
}
582+
#disable-next-line BCP037
583+
siteUpdateStrategy: {
584+
type: 'RollingUpdate'
585+
}
556586
}
557587
siteConfig: {
558588
vnetRouteAllEnabled: true
559589
cors: {
560590
allowedOrigins: corsAllowedOrigins
561591
supportCredentials: true
562592
}
563-
appSettings: concat(appSettingsBase, corsAppSettings, telemetryTuningAppSettings, [
593+
appSettings: concat(appSettingsBase, southeastAsiaCosmosPreferredRegionAppSettings, corsAppSettings, telemetryTuningAppSettings, [
564594
{
565595
name: 'AzureWebJobsStorage__accountName'
566596
value: storageSoutheastAsia.name

src/host/WriteFluency.AppHost/aspirate-overlays-propositions/wf-api/deployment-patch.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ kind: Deployment
33
metadata:
44
name: wf-propositions-api
55
spec:
6+
replicas: 2
67
strategy:
78
type: RollingUpdate
89
rollingUpdate:
@@ -14,6 +15,22 @@ spec:
1415
- name: ghcr-secret
1516
containers:
1617
- name: wf-propositions-api
18+
readinessProbe:
19+
httpGet:
20+
path: /health
21+
port: 8080
22+
initialDelaySeconds: 10
23+
periodSeconds: 10
24+
timeoutSeconds: 3
25+
failureThreshold: 6
26+
livenessProbe:
27+
httpGet:
28+
path: /alive
29+
port: 8080
30+
initialDelaySeconds: 30
31+
periodSeconds: 30
32+
timeoutSeconds: 3
33+
failureThreshold: 3
1734
envFrom:
1835
- configMapRef:
1936
name: wf-propositions-api-env

src/host/WriteFluency.AppHost/aspirate-overlays-users/wf-users-api/deployment-patch.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ kind: Deployment
33
metadata:
44
name: wf-users-api
55
spec:
6+
replicas: 2
67
strategy:
78
type: RollingUpdate
89
rollingUpdate:
@@ -14,6 +15,22 @@ spec:
1415
- name: ghcr-secret
1516
containers:
1617
- name: wf-users-api
18+
readinessProbe:
19+
httpGet:
20+
path: /health
21+
port: 8080
22+
initialDelaySeconds: 10
23+
periodSeconds: 10
24+
timeoutSeconds: 3
25+
failureThreshold: 6
26+
livenessProbe:
27+
httpGet:
28+
path: /alive
29+
port: 8080
30+
initialDelaySeconds: 30
31+
periodSeconds: 30
32+
timeoutSeconds: 3
33+
failureThreshold: 3
1734
envFrom:
1835
- configMapRef:
1936
name: wf-users-api-env

src/propositions-service/WriteFluency.Infrastructure/Http/Services/ArticleExtractor.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ public async Task<Result<string>> GetVisibleTextAsync(string url, CancellationTo
6565

6666
return result;
6767
}
68+
catch (HttpRequestException ex)
69+
{
70+
_logger.LogWarning(
71+
"Article content was rejected or unavailable. Url={Url}, StatusCode={StatusCode}.",
72+
url,
73+
ex.StatusCode);
74+
75+
return Result.Fail(new Error("Failed to extract visible text").CausedBy(ex));
76+
}
6877
catch (Exception ex)
6978
{
7079
_logger.LogError(ex, "Failed to extract visible text from {Url}", url);

src/users-progress-service/WriteFluency.UsersProgressService/Functions/UserProgressFunctions.cs

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,15 @@ public async Task<HttpResponseData> StartAsync(
7272
return await CreateJsonAsync(request, HttpStatusCode.BadRequest, new { Error = "exercise_id_invalid" }, cancellationToken);
7373
}
7474

75-
var response = await _progressTrackingService.StartAsync(auth.UserId, body, cancellationToken);
75+
ProgressOperationResponse response;
76+
try
77+
{
78+
response = await _progressTrackingService.StartAsync(auth.UserId, body, cancellationToken);
79+
}
80+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
81+
{
82+
return CreateClientClosedRequestResponse(request, "ProgressStart", auth.UserId);
83+
}
7684

7785
_logger.LogInformation(
7886
"Progress start request processed. UserId={UserId}, ExerciseId={ExerciseId}, Status={Status}, TrackingEnabled={TrackingEnabled}.",
@@ -128,7 +136,15 @@ public async Task<HttpResponseData> CompleteAsync(
128136
return await CreateJsonAsync(request, HttpStatusCode.BadRequest, new { Error = "exercise_id_invalid" }, cancellationToken);
129137
}
130138

131-
var response = await _progressTrackingService.CompleteAsync(auth.UserId, body, cancellationToken);
139+
ProgressOperationResponse response;
140+
try
141+
{
142+
response = await _progressTrackingService.CompleteAsync(auth.UserId, body, cancellationToken);
143+
}
144+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
145+
{
146+
return CreateClientClosedRequestResponse(request, "ProgressComplete", auth.UserId);
147+
}
132148

133149
_logger.LogInformation(
134150
"Progress complete request processed. UserId={UserId}, ExerciseId={ExerciseId}, Status={Status}, TrackingEnabled={TrackingEnabled}.",
@@ -161,7 +177,15 @@ public async Task<HttpResponseData> SummaryAsync(
161177
return await CreateJsonAsync(request, HttpStatusCode.Unauthorized, new { Error = "unauthorized" }, cancellationToken);
162178
}
163179

164-
var response = await _progressTrackingService.GetSummaryAsync(auth.UserId, cancellationToken);
180+
ProgressSummaryResponse response;
181+
try
182+
{
183+
response = await _progressTrackingService.GetSummaryAsync(auth.UserId, cancellationToken);
184+
}
185+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
186+
{
187+
return CreateClientClosedRequestResponse(request, "ProgressSummary", auth.UserId);
188+
}
165189

166190
_logger.LogInformation(
167191
"Progress summary request processed. UserId={UserId}, TotalItems={TotalItems}, InProgressCount={InProgressCount}, CompletedCount={CompletedCount}, TotalAttempts={TotalAttempts}.",
@@ -208,7 +232,15 @@ public async Task<HttpResponseData> StateAsync(
208232
return await CreateJsonAsync(request, HttpStatusCode.BadRequest, new { Error = "exercise_id_invalid" }, cancellationToken);
209233
}
210234

211-
var response = await _progressTrackingService.GetStateAsync(auth.UserId, exerciseId, cancellationToken);
235+
ProgressStateResponse response;
236+
try
237+
{
238+
response = await _progressTrackingService.GetStateAsync(auth.UserId, exerciseId, cancellationToken);
239+
}
240+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
241+
{
242+
return CreateClientClosedRequestResponse(request, "ProgressState", auth.UserId);
243+
}
212244

213245
_logger.LogInformation(
214246
"Progress state request processed. UserId={UserId}, ExerciseId={ExerciseId}, HasServerState={HasServerState}, TrackingEnabled={TrackingEnabled}.",
@@ -241,7 +273,15 @@ public async Task<HttpResponseData> ItemsAsync(
241273
return await CreateJsonAsync(request, HttpStatusCode.Unauthorized, new { Error = "unauthorized" }, cancellationToken);
242274
}
243275

244-
var response = await _progressTrackingService.GetItemsAsync(auth.UserId, cancellationToken);
276+
IReadOnlyList<ProgressItemResponse> response;
277+
try
278+
{
279+
response = await _progressTrackingService.GetItemsAsync(auth.UserId, cancellationToken);
280+
}
281+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
282+
{
283+
return CreateClientClosedRequestResponse(request, "ProgressItems", auth.UserId);
284+
}
245285

246286
_logger.LogInformation(
247287
"Progress items request processed. UserId={UserId}, ItemCount={ItemCount}.",
@@ -280,7 +320,15 @@ public async Task<HttpResponseData> AttemptsAsync(
280320
exerciseId = parsedExerciseId;
281321
}
282322

283-
var response = await _progressTrackingService.GetAttemptsAsync(auth.UserId, exerciseId, cancellationToken);
323+
IReadOnlyList<ProgressAttemptResponse> response;
324+
try
325+
{
326+
response = await _progressTrackingService.GetAttemptsAsync(auth.UserId, exerciseId, cancellationToken);
327+
}
328+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
329+
{
330+
return CreateClientClosedRequestResponse(request, "ProgressAttempts", auth.UserId);
331+
}
284332

285333
_logger.LogInformation(
286334
"Progress attempts request processed. UserId={UserId}, ExerciseId={ExerciseId}, AttemptCount={AttemptCount}.",
@@ -335,7 +383,15 @@ public async Task<HttpResponseData> SaveStateAsync(
335383
return await CreateJsonAsync(request, HttpStatusCode.BadRequest, new { Error = "exercise_id_invalid" }, cancellationToken);
336384
}
337385

338-
var response = await _progressTrackingService.SaveStateAsync(auth.UserId, body, cancellationToken);
386+
ProgressOperationResponse response;
387+
try
388+
{
389+
response = await _progressTrackingService.SaveStateAsync(auth.UserId, body, cancellationToken);
390+
}
391+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
392+
{
393+
return CreateClientClosedRequestResponse(request, "ProgressStateSave", auth.UserId);
394+
}
339395

340396
_logger.LogInformation(
341397
"Progress save-state request processed. UserId={UserId}, ExerciseId={ExerciseId}, Status={Status}, TrackingEnabled={TrackingEnabled}.",
@@ -409,6 +465,20 @@ private static bool IsAllowedOrigin(string uriLikeHeaderValue, HashSet<string> a
409465
return values.FirstOrDefault();
410466
}
411467

468+
private HttpResponseData CreateClientClosedRequestResponse(
469+
HttpRequestData request,
470+
string operationName,
471+
string userId)
472+
{
473+
_logger.LogInformation(
474+
"Progress request was canceled by the client. Operation={OperationName}, UserId={UserId}, Path={Path}.",
475+
operationName,
476+
userId,
477+
request.Url.AbsolutePath);
478+
479+
return request.CreateResponse((HttpStatusCode)499);
480+
}
481+
412482
private static async Task<HttpResponseData> CreateJsonAsync(
413483
HttpRequestData request,
414484
HttpStatusCode statusCode,

0 commit comments

Comments
 (0)