@detiq/crawler 0.2.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/crawler.ts +38 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
6
6
  "type": "module",
package/src/crawler.ts CHANGED
@@ -3109,9 +3109,19 @@ export async function crawlProject(opts: {
3109
3109
  await onProgress?.(90, `App map saved · ${visited.size} screens, ${edges.length} flows wired`);
3110
3110
  }
3111
3111
 
3112
+ // P5: Export session cookies BEFORE session health check so we can reuse them even if
3113
+ // the main browser session is stale. The cookie jar stays valid for fresh browser contexts
3114
+ // even when the CDP-connected Stagehand page shows a redirect to /login.
3115
+ let mainStorageState: any;
3116
+ try {
3117
+ mainStorageState = await playwrightBrowser!.contexts()[0]?.storageState();
3118
+ } catch { /* non-fatal */ }
3119
+
3112
3120
  // P5: Orphan page auth re-verify — confirm session is still alive before tail-end viewport pass.
3113
3121
  // The main BFS can span many minutes; the session may have expired by the time we reach here.
3114
- // If expired, attempt re-auth via Stagehand before proceeding. Only skip if re-auth also fails.
3122
+ // If expired, attempt re-auth via Stagehand before proceeding.
3123
+ // Creative resilience: even if the main browser is dead, we still run the viewport pass using
3124
+ // the exported cookie jar — fresh browsers are often accepted by servers even after CDP session loss.
3115
3125
  let sessionAliveForViewports = true;
3116
3126
  try {
3117
3127
  await page.goto(opts.appUrl, { waitUntil: 'domcontentloaded', timeout: 15_000 });
@@ -3130,19 +3140,24 @@ export async function crawlProject(opts: {
3130
3140
  ).catch(() => false);
3131
3141
  }
3132
3142
  if (reauthed) {
3143
+ // Refresh cookie jar with newly authenticated session
3144
+ try { mainStorageState = await playwrightBrowser!.contexts()[0]?.storageState(); } catch { /* keep prior */ }
3133
3145
  logger.info('[P5] Re-auth succeeded — proceeding with viewport pass');
3134
3146
  } else {
3135
3147
  sessionAliveForViewports = false;
3136
- logger.warn({ landed: sessionCheckUrl }, '[P5] Re-auth failed — skipping mobile/tablet screenshots');
3137
- AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
3138
- jobId: opts.jobId,
3139
- type: 'AUTH_FAILURE',
3140
- payload: {
3141
- url: opts.appUrl,
3142
- landed: sessionCheckUrl,
3143
- reason: 'Session expired before mobile/tablet viewport pass and re-auth failed. Re-capture the session in project settings.',
3144
- },
3145
- }).catch(() => { });
3148
+ const hasCookies = (mainStorageState?.cookies?.length ?? 0) > 0;
3149
+ logger.warn({ landed: sessionCheckUrl, hasCookies }, '[P5] Re-auth failed — will attempt viewport pass with exported cookies');
3150
+ if (!hasCookies) {
3151
+ AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
3152
+ jobId: opts.jobId,
3153
+ type: 'AUTH_FAILURE',
3154
+ payload: {
3155
+ url: opts.appUrl,
3156
+ landed: sessionCheckUrl,
3157
+ reason: 'Session expired before mobile/tablet viewport pass and re-auth failed. Re-capture the session in project settings.',
3158
+ },
3159
+ }).catch(() => { });
3160
+ }
3146
3161
  }
3147
3162
  } else {
3148
3163
  logger.info({ url: sessionCheckUrl }, '[P5] Session alive — proceeding with viewport pass');
@@ -3162,7 +3177,10 @@ export async function crawlProject(opts: {
3162
3177
  // (isMobile, hasTouch, viewport, deviceScaleFactor) on new contexts — the flags are
3163
3178
  // silently ignored, so screenshots come out at desktop dimensions regardless of profile.
3164
3179
  const extraProfiles = (opts.deviceProfiles ?? []).filter((p) => p !== 'DESKTOP');
3165
- if (sessionAliveForViewports && extraProfiles.length > 0 && discovered.length > 0) {
3180
+ // Run the viewport pass even when the main session is dead, as long as we exported cookies —
3181
+ // fresh browsers often authenticate successfully with the saved cookie jar.
3182
+ const hasExportedAuth = (mainStorageState?.cookies?.length ?? 0) > 0;
3183
+ if ((sessionAliveForViewports || hasExportedAuth) && extraProfiles.length > 0 && discovered.length > 0) {
3166
3184
  const { devices, chromium: pwChromium } = await import('playwright');
3167
3185
  const DEVICE_MAP: Record<string, any> = {
3168
3186
  // Override viewport heights: Playwright's device descriptors use viewport-minus-browser-chrome
@@ -3171,11 +3189,6 @@ export async function crawlProject(opts: {
3171
3189
  MOBILE_PORTRAIT: { ...devices['iPhone 15'], viewport: { width: 393, height: 852 } },
3172
3190
  TABLET: { ...devices['iPad Pro 11'], viewport: { width: 834, height: 1194 } },
3173
3191
  };
3174
- // Export session cookies from the main crawl so mobile browser is already authenticated
3175
- let mainStorageState: any;
3176
- try {
3177
- mainStorageState = await playwrightBrowser!.contexts()[0]?.storageState();
3178
- } catch { /* non-fatal — mobile pass will proceed without auth */ }
3179
3192
 
3180
3193
  for (const profile of extraProfiles) {
3181
3194
  const deviceConfig = DEVICE_MAP[profile];
@@ -3224,13 +3237,19 @@ export async function crawlProject(opts: {
3224
3237
  ).catch(() => { });
3225
3238
  }
3226
3239
  } catch (err) {
3227
- console.warn(`[crawler] ${profile} pass failed for ${disc.url}:`, err);
3240
+ logger.warn({ profile, url: disc.url, err: String(err) }, `[P5] ${profile} screenshot failed — skipping screen`);
3241
+ AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
3242
+ jobId: opts.jobId,
3243
+ screenId: disc.screenId,
3244
+ type: 'VIEWPORT_SCREENSHOT_FAILURE',
3245
+ payload: { profile, url: disc.url, reason: String(err) },
3246
+ }).catch(() => { });
3228
3247
  }
3229
3248
  }
3230
3249
  await mobilePage.close().catch(() => { });
3231
3250
  await mobileCtx.close().catch(() => { });
3232
3251
  } catch (profileErr) {
3233
- console.warn(`[crawler] ${profile} browser launch failed:`, profileErr);
3252
+ logger.warn({ profile, err: String(profileErr) }, `[P5] ${profile} browser launch failed`);
3234
3253
  } finally {
3235
3254
  await mobileBrowser?.close().catch(() => { });
3236
3255
  }