Skip to content

Commit 24872ac

Browse files
Merge pull request #335 from Nick-1234531/DOT-5046/add-screenshot-sync-status
Dot 5046/add screenshot sync status
2 parents deac3fa + 8265cfe commit 24872ac

File tree

6 files changed

+242
-10
lines changed

6 files changed

+242
-10
lines changed

src/lib/httpClient.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export default class httpClient {
321321
}, ctx.log)
322322
}
323323

324-
processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, discoveryErrors: DiscoveryErrors) {
324+
processSnapshot(ctx: Context, snapshot: ProcessedSnapshot, snapshotUuid: string, discoveryErrors: DiscoveryErrors, variantCount: number, sync: boolean = false) {
325325
return this.request({
326326
url: `/build/${ctx.build.id}/snapshot`,
327327
method: 'POST',
@@ -330,12 +330,14 @@ export default class httpClient {
330330
name: snapshot.name,
331331
url: snapshot.url,
332332
snapshotUuid: snapshotUuid,
333+
variantCount: variantCount,
333334
test: {
334335
type: ctx.testType,
335336
source: 'cli'
336337
},
337338
doRemoteDiscovery: snapshot.options.doRemoteDiscovery,
338339
discoveryErrors: discoveryErrors,
340+
sync: sync
339341
}
340342
}, ctx.log)
341343
}
@@ -623,4 +625,14 @@ export default class httpClient {
623625
data: requestData
624626
}, ctx.log)
625627
}
628+
629+
getSnapshotStatus(snapshotName: string, snapshotUuid: string, ctx: Context): Promise<Record<string, any>> {
630+
return this.request({
631+
url: `/snapshot/status?buildId=${ctx.build.id}&snapshotName=${snapshotName}&snapshotUUID=${snapshotUuid}`,
632+
method: 'GET',
633+
headers: {
634+
'Content-Type': 'application/json',
635+
}
636+
}, ctx.log);
637+
}
626638
}

src/lib/schemaValidation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,18 @@ const SnapshotSchema: JSONSchemaType<Snapshot> = {
501501
sessionId: {
502502
type: "string",
503503
errorMessage: "Invalid snapshot options; sessionId must be a string"
504+
},
505+
contextId: {
506+
type: "string",
507+
errorMessage: "Invalid snapshot options; contextId must be a string"
508+
},
509+
sync: {
510+
type: "boolean",
511+
errorMessage: "Invalid snapshot options; sync must be a boolean"
512+
},
513+
timeout: {
514+
type: "number",
515+
errorMessage: "Invalid snapshot options; timeout must be a number"
504516
}
505517
},
506518
additionalProperties: false

src/lib/server.ts

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
4141
// Fetch sessionId from snapshot options if present
4242
const sessionId = snapshot?.options?.sessionId;
4343
let capsBuildId = ''
44+
const contextId = snapshot?.options?.contextId;
4445

4546
if (sessionId) {
4647
// Check if sessionId exists in the map
@@ -71,7 +72,23 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
7172
}
7273

7374
ctx.testType = testType;
74-
ctx.snapshotQueue?.enqueue(snapshot);
75+
76+
if (contextId && !ctx.contextToSnapshotMap) {
77+
ctx.contextToSnapshotMap = new Map();
78+
ctx.log.debug(`Initialized empty context mapping map for contextId: ${contextId}`);
79+
}
80+
81+
if (contextId && ctx.contextToSnapshotMap) {
82+
ctx.contextToSnapshotMap.set(contextId, 0);
83+
ctx.log.debug(`Marking contextId as captured and added to queue: ${contextId}`);
84+
}
85+
86+
if(contextId){
87+
ctx.snapshotQueue?.enqueueFront(snapshot);
88+
}else{
89+
ctx.snapshotQueue?.enqueue(snapshot);
90+
}
91+
7592
ctx.isSnapshotCaptured = true;
7693
replyCode = 200;
7794
replyBody = { data: { message: "success", warnings: [] }};
@@ -130,7 +147,113 @@ export default async (ctx: Context): Promise<FastifyInstance<Server, IncomingMes
130147
reply.code(200).send({ status: 'Server is running', version: ctx.cliVersion });
131148
});
132149

133-
150+
// Get snapshot status
151+
server.get('/snapshot/status', opts, async (request, reply) => {
152+
let replyCode: number;
153+
let replyBody: Record<string, any>;
154+
155+
156+
try {
157+
ctx.log.debug(`request.query : ${JSON.stringify(request.query)}`);
158+
const { contextId, pollTimeout, snapshotName } = request.query as { contextId: string, pollTimeout: number, snapshotName: string };
159+
if (!contextId || !snapshotName) {
160+
throw new Error('contextId and snapshotName are required parameters');
161+
}
162+
163+
const timeoutDuration = pollTimeout*1000 || 30000;
164+
165+
// Check if we have stored snapshot status for this contextId
166+
if (ctx.contextToSnapshotMap?.has(contextId)) {
167+
let contextStatus = ctx.contextToSnapshotMap.get(contextId);
168+
169+
while (contextStatus==0) {
170+
// Wait 5 seconds before next check
171+
await new Promise(resolve => setTimeout(resolve, 5000));
172+
173+
contextStatus = ctx.contextToSnapshotMap.get(contextId);
174+
}
175+
176+
if(contextStatus==2){
177+
throw new Error("Snapshot Failed");
178+
}
179+
180+
ctx.log.debug("Snapshot uploaded successfully");
181+
182+
// Poll external API until it returns 200 or timeout is reached
183+
let lastExternalResponse: any = null;
184+
const startTime = Date.now();
185+
186+
while (true) {
187+
try {
188+
const externalResponse = await ctx.client.getSnapshotStatus(
189+
snapshotName,
190+
contextId,
191+
ctx
192+
);
193+
194+
lastExternalResponse = externalResponse;
195+
196+
if (externalResponse.statusCode === 200) {
197+
replyCode = 200;
198+
replyBody = externalResponse.data;
199+
return reply.code(replyCode).send(replyBody);
200+
} else if (externalResponse.statusCode === 202 ) {
201+
replyBody= externalResponse.data;
202+
ctx.log.debug(`External API attempt: Still processing, Pending Screenshots ${externalResponse.snapshotCount}`);
203+
await new Promise(resolve => setTimeout(resolve, 5000));
204+
}else if(externalResponse.statusCode===404){
205+
ctx.log.debug(`Snapshot still processing, not uploaded`);
206+
await new Promise(resolve => setTimeout(resolve, 5000));
207+
}else {
208+
ctx.log.debug(`Unexpected response from external API: ${JSON.stringify(externalResponse)}`);
209+
replyCode = 500;
210+
replyBody = {
211+
error: {
212+
message: `Unexpected response from external API: ${externalResponse.statusCode}`,
213+
externalApiStatus: externalResponse.statusCode
214+
}
215+
};
216+
return reply.code(replyCode).send(replyBody);
217+
}
218+
219+
ctx.log.debug(`timeoutDuration: ${timeoutDuration}`);
220+
ctx.log.debug(`Time passed: ${Date.now() - startTime}`);
221+
222+
if (Date.now() - startTime > timeoutDuration) {
223+
replyCode = 202;
224+
replyBody = {
225+
data: {
226+
message: 'Request timed out-> Snapshot still processing'
227+
}
228+
};
229+
return reply.code(replyCode).send(replyBody);
230+
}
231+
232+
} catch (externalApiError: any) {
233+
ctx.log.debug(`External API call failed: ${externalApiError.message}`);
234+
replyCode = 500;
235+
replyBody = {
236+
error: {
237+
message: `External API call failed: ${externalApiError.message}`
238+
}
239+
};
240+
return reply.code(replyCode).send(replyBody);
241+
}
242+
}
243+
} else {
244+
// No snapshot found for this contextId
245+
replyCode = 404;
246+
replyBody = { error: { message: `No snapshot found for contextId: ${contextId}` } };
247+
return reply.code(replyCode).send(replyBody);
248+
}
249+
} catch (error: any) {
250+
ctx.log.debug(`snapshot status failed; ${error}`);
251+
replyCode = 500;
252+
replyBody = { error: { message: error.message } };
253+
return reply.code(replyCode).send(replyBody);
254+
}
255+
});
256+
134257

135258
await server.listen({ port: ctx.options.port });
136259
// store server's address for SDK

src/lib/snapshotQueue.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Snapshot, Context } from "../types.js";
33
import constants from "./constants.js";
44
import processSnapshot, {prepareSnapshot} from "./processSnapshot.js"
55
import { v4 as uuidv4 } from 'uuid';
6-
import { startPolling, stopTunnelHelper } from "./utils.js";
6+
import { startPolling, stopTunnelHelper, calculateVariantCountFromSnapshot } from "./utils.js";
77

88
const uploadDomToS3ViaEnv = process.env.USE_LAMBDA_INTERNAL || false;
99
export default class Queue {
@@ -29,6 +29,16 @@ export default class Queue {
2929
}
3030
}
3131

32+
enqueueFront(item: Snapshot): void {
33+
this.snapshots.unshift(item);
34+
if (!this.ctx.config.delayedUpload) {
35+
if (!this.processing) {
36+
this.processing = true;
37+
this.processNext();
38+
}
39+
}
40+
}
41+
3242
startProcessingfunc(): void {
3343
if (!this.processing) {
3444
this.processing = true;
@@ -129,6 +139,8 @@ export default class Queue {
129139
return drop;
130140
}
131141

142+
143+
132144
private filterVariants(snapshot: Snapshot, config: any): boolean {
133145
let allVariantsDropped = true;
134146

@@ -273,6 +285,7 @@ export default class Queue {
273285
this.processingSnapshot = snapshot?.name;
274286
let drop = false;
275287

288+
276289
if (this.ctx.isStartExec && !this.ctx.config.tunnel) {
277290
this.ctx.log.info(`Processing Snapshot: ${snapshot?.name}`);
278291
}
@@ -332,6 +345,7 @@ export default class Queue {
332345

333346

334347
if (useCapsBuildId) {
348+
this.ctx.log.info(`Using cached buildId: ${capsBuildId}`);
335349
if (useKafkaFlowCaps) {
336350
const snapshotUuid = uuidv4();
337351
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
@@ -378,18 +392,23 @@ export default class Queue {
378392
}
379393
}
380394
if (this.ctx.build && this.ctx.build.useKafkaFlow) {
381-
const snapshotUuid = uuidv4();
395+
let snapshotUuid = uuidv4();
382396
let snapshotUploadResponse
383-
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
384-
if (!uploadDomToS3) {
397+
if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)) {
398+
snapshotUuid = snapshot.options.contextId;
399+
}
400+
let uploadDomToS3 = this.ctx.config.useLambdaInternal || uploadDomToS3ViaEnv;
401+
if (!uploadDomToS3) {
385402
this.ctx.log.debug(`Uploading dom to S3 for snapshot using presigned URL`);
386403
const presignedResponse = await this.ctx.client.getS3PresignedURLForSnapshotUpload(this.ctx, processedSnapshot.name, snapshotUuid);
387404
const uploadUrl = presignedResponse.data.url;
388405
snapshotUploadResponse = await this.ctx.client.uploadSnapshotToS3(this.ctx, uploadUrl, processedSnapshot);
389-
} else {
406+
} else {
390407
this.ctx.log.debug(`Uploading dom to S3 for snapshot using LSRS`);
391408
snapshotUploadResponse = await this.ctx.client.sendDomToLSRS(this.ctx, processedSnapshot, snapshotUuid);
392-
}
409+
}
410+
411+
393412
if (!snapshotUploadResponse || Object.keys(snapshotUploadResponse).length === 0) {
394413
this.ctx.log.debug(`snapshot failed; Unable to upload dom to S3`);
395414
this.processedSnapshots.push({ name: snapshot?.name, error: `snapshot failed; Unable to upload dom to S3` });
@@ -403,11 +422,19 @@ export default class Queue {
403422
this.ctx.log.debug(`Closed browser context for snapshot ${snapshot.name}`);
404423
}
405424
}
425+
if(snapshot?.options?.contextId){
426+
this.ctx.contextToSnapshotMap?.set(snapshot?.options?.contextId,2);
427+
}
406428
this.processNext();
407429
} else {
408-
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors);
430+
await this.ctx.client.processSnapshot(this.ctx, processedSnapshot, snapshotUuid, discoveryErrors,calculateVariantCountFromSnapshot(processedSnapshot, this.ctx.config),snapshot?.options?.sync);
431+
if(snapshot?.options?.contextId && this.ctx.contextToSnapshotMap?.has(snapshot.options.contextId)){
432+
this.ctx.contextToSnapshotMap.set(snapshot.options.contextId, 1);
433+
}
434+
this.ctx.log.debug(`ContextId: ${snapshot?.options?.contextId} status set to uploaded`);
409435
}
410436
} else {
437+
this.ctx.log.info(`Uploading snapshot to S3`);
411438
await this.ctx.client.uploadSnapshot(this.ctx, processedSnapshot, discoveryErrors);
412439
}
413440
this.ctx.totalSnapshots++;
@@ -418,6 +445,9 @@ export default class Queue {
418445
} catch (error: any) {
419446
this.ctx.log.debug(`snapshot failed; ${error}`);
420447
this.processedSnapshots.push({ name: snapshot?.name, error: error.message });
448+
if (snapshot?.options?.contextId && this.ctx.contextToSnapshotMap) {
449+
this.ctx.contextToSnapshotMap.set(snapshot.options.contextId, 2);
450+
}
421451
}
422452
// Close open browser contexts and pages
423453
if (this.ctx.browser) {

src/lib/utils.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,5 +456,57 @@ export async function stopTunnelHelper(ctx: Context) {
456456
ctx.log.debug('Tunnel is Stopped ? ' + status);
457457
}
458458

459+
/**
460+
* Calculate the number of variants for a snapshot based on the configuration
461+
* @param config - The configuration object containing web and mobile settings
462+
* @returns The total number of variants that would be generated
463+
*/
464+
export function calculateVariantCount(config: any): number {
465+
let variantCount = 0;
466+
467+
// Calculate web variants
468+
if (config.web) {
469+
const browsers = config.web.browsers || [];
470+
const viewports = config.web.viewports || [];
471+
variantCount += browsers.length * viewports.length;
472+
}
473+
474+
// Calculate mobile variants
475+
if (config.mobile) {
476+
const devices = config.mobile.devices || [];
477+
variantCount += devices.length;
478+
}
479+
480+
return variantCount;
481+
}
482+
483+
/**
484+
* Calculate the number of variants for a snapshot based on snapshot-specific options
485+
* @param snapshot - The snapshot object with options
486+
* @param globalConfig - The global configuration object (fallback)
487+
* @returns The total number of variants that would be generated
488+
*/
489+
export function calculateVariantCountFromSnapshot(snapshot: any, globalConfig?: any): number {
490+
let variantCount = 0;
491+
492+
493+
// Check snapshot-specific web options
494+
if (snapshot.options?.web) {
495+
const browsers = snapshot.options.web.browsers || [];
496+
const viewports = snapshot.options.web.viewports || [];
497+
variantCount += browsers.length * viewports.length;
498+
}
459499

500+
// Check snapshot-specific mobile options
501+
if (snapshot.options?.mobile) {
502+
const devices = snapshot.options.mobile.devices || [];
503+
variantCount += devices.length;
504+
}
505+
506+
// Fallback to global config if no snapshot-specific options
507+
if (variantCount === 0 && globalConfig) {
508+
variantCount = calculateVariantCount(globalConfig);
509+
}
460510

511+
return variantCount;
512+
}

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export interface Context {
8787
mergeBuildTargetId?: string;
8888
mergeByBranch?: boolean;
8989
mergeByBuild?: boolean;
90+
contextToSnapshotMap?: Map<string, number>;
9091
}
9192

9293
export interface Env {
@@ -147,6 +148,8 @@ export interface Snapshot {
147148
loadDomContent?: boolean;
148149
ignoreType?: string[],
149150
sessionId?: string
151+
sync?: boolean;
152+
contextId?: string;
150153
}
151154
}
152155

0 commit comments

Comments
 (0)