mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
## What
Stabilizes the **SDR**, **GeneralTests** and **Metrics** shards of the
alpha1 cloud
E2E suite (`playwright_alpha1.yml`) to green — the last three shards
remaining after
the rest of the suite was greened. Rebased on `main`.
The original failing run had, across these shards: **SDR** 1 hard fail +
1 flake,
**GeneralTests** 21 fails + 2 flakes, **Metrics** a load-sensitive
query-persistence
flake. Root causes were cloud data/UI-readiness races (freshly-created
streams &
regex patterns lagging their list/table; browser OOM under parallelism;
revamped Reka
UI re-rendering mid-interaction), all fixed with deterministic waits on
real signals.
**No `waitForTimeout` as a fix, no `force:true`, no `.first()`/`.nth()`
band-aids, no
retry-count bumps to hide failures, no weakened assertions. No
worker-count,
parallelism, playwright-config or CI/yml changes** — test code only.
## Root cause & fixes
**SDR**
1. **`scrollIntoViewIfNeeded` 45s timeout** (`multipleSDRPatterns`,
`queryTimeDrop`).
The revamped Reka streams/schema table re-renders rows mid-animation, so
the
scroll's stability polling spun to the 45s action timeout even though
the element
was visible. Guard with `expect(el).toBeVisible()` + an instant native
`scrollIntoView`; removed the trailing sleeps.
(`streamAssociationPage.js`)
2. **Stream Detail `click()` 45s timeout.** One-shot JS-then-Playwright
click raced the
re-rendering row. Replaced with a `toPass` retry that re-resolves the
button fresh,
fires a native DOM click, and verifies the detail sidebar opened.
3. **Regex-pattern create→list eventual consistency** (6 combined tests:
`ingestionTime*`,
`queryTime*`). Create is accepted (`verifyPatternCreatedSuccess` passes)
before it
propagates to the list read-replica, so the UI text-count check threw
"not found in list". Gate on the backend source of truth:
`waitForPatternCreated`
polls `GET /re_patterns` until present then confirms the UI rendered;
`waitForPatternDeleted` fixes the symmetric "Unable to delete … Reason:
unknown"
delete race. (`sdrPatternsPage.js`)
4. **Freshly-ingested stream lags the `/streams` table**
(`multipleSDRPatterns` @slow).
`openStreamDetail` reloads + re-searches until the stream's row appears
(bounded),
instead of a one-shot 5s wait. (Local-caught gotcha:
`isVisible({timeout})` does
**not** auto-wait — switched to `waitFor({state:'visible'})`.)
5. Transient cloud-gateway **connection timeouts** on `page.request`
pattern calls wrapped
in a connection-error-only retry.
**GeneralTests**
6. **cipherKeys ×6 — tab never visible.** Cipher Keys shares the exact
`visible: isEnt`
gate as Regex Patterns (`settings/index.vue`), and SDR's regex
management passes on
this same cloud env — so the tab **does** render; the test was clicking
the Settings
menu, which lands on the SectionHub where per-tab data-tests aren't
rendered. Fixed
by a **direct `goto('/web/settings/cipher_keys')`**, mirroring the
working regex page.
(Not skipped — that would have masked a real, fixable nav bug.)
7. **enrichment ×7 — `pipeline-section-tab-enrichmentTables` 45s
force-click.** The tab
only mounts after the pipelines page header renders; the
flyout→pipelines→section-tab
nav is fragile under load. Replaced with direct SPA nav to the
enrichment-tables route
+ `goto` fallback + a `toPass` settle-retry Add-button click.
(`pipelinesPage.js`)
8. **languageTranslation ×8 + landingPage ×1 — browser OOM cascade**
("Target page/context/
browser has been closed"). 10 exhaustive ~27-page full-app crawls ran
`mode:parallel`
under `--workers=5`; a worker's browser was OOM-killed and every test
after it
cascaded. **Kept parallel** (serializing overran the shard timeout) —
the crawl now
navigates `waitUntil:'domcontentloaded'` + a bounded settle instead of
waiting up to
10–30s for `networkidle` on chart/websocket-heavy pages, cutting each
heavy page's
peak-memory dwell ~10s→~3s (so 5 concurrent crawls no longer spike
together) and making
each crawl ~3–4× faster. Added fail-fast on a closed page/context so any
residual crash
surfaces at its true origin instead of being swallowed/retried.
9. **schema ×2 — `selectStream` can't find just-created
`test1`/`test3`.** Local reproduction
proved the root cause: the **fixed** stream names hit `400 "stream is
being deleted"`
(a prior run's cleanup still finishing) → the stream got no data → never
listed. Fixed
with **unique per-invocation stream names**; also hardened the ingestion
helper
(status-check + transient retry) and added a streams-API readiness gate
before
`selectStream`. (`schema.spec.js`)
10. **enrichment `@upload` ×2 — `_search` `waitForResponse` timeout.**
The 30s search wait
was armed **before** the up-to-120s `selectStream`, so it expired
mid-selection. Re-armed
it (200-gated) immediately before `applyQuery`, plus a streams-API
readiness gate for
`e2e_automate`. `schemaload` gated on the `streams?type=logs` response
before reload.
**Metrics**
11. Replaced `waitForTimeout`s in `metricsPage.js` with real signals
(`data-selected="true"`,
`data-state="on"`) and fixed a reka `OToggleGroupItem` state-detection
bug (active state
is `data-state="on"`, not a `selected` class).
12. **`metrics-promql-query-persistence` load-sensitive flake.**
`selectFirstStream` returned
before the async auto-populate finished, and persistence tests read the
query editor
before Monaco re-rendered after tab switches. Added editor-readiness
gates on real
signals (auto-populated query present; Monaco mounted + laid out) in
`metricsQueryEditorPage.js`; assertions unchanged.
## Validation
- **CI — SDR / GeneralTests / Metrics green (alpha cloud):**
https://github.com/openobserve/o2-enterprise/actions/runs/29633444186
- **Original failing run:**
https://github.com/openobserve/o2-enterprise/actions/runs/29481437880
- **Local (alpha cloud, CI conditions, single-worker):** each
previously-failing test re-run
to green — `schema.spec.js` 3/3, `multipleSDRPatterns.spec.js` 2/2.
## Notes
- 21 test files change (8 page objects + 13 specs). No shared
cleanup/setup, config, or
workflow files touched.
- RegressionSet was unaffected by this branch — a transient hang in the
workflow's
"pre-test cleanup" step (before any test runs), unrelated to these
changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
260 lines
12 KiB
JavaScript
260 lines
12 KiB
JavaScript
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
|
const PageManager = require('../../pages/page-manager.js');
|
|
const testLogger = require('../utils/test-logger.js');
|
|
const logsdata = require("../../../test-data/logs_data.json");
|
|
const { getAuthHeaders, getOrgIdentifier } = require('../utils/cloud-auth.js');
|
|
|
|
test.describe.configure({ mode: 'parallel' });
|
|
|
|
test.describe("Schema testcases", () => {
|
|
let pm;
|
|
const streamName = `stream${Date.now()}`;
|
|
|
|
// Ingestion helper function
|
|
async function ingestion(page, testStreamName, maxRetries = 5) {
|
|
testLogger.debug('Starting ingestion for test stream', { testStreamName });
|
|
const orgId = getOrgIdentifier();
|
|
|
|
const headers = {
|
|
...getAuthHeaders(),
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
// Retry until the ingest is ACCEPTED (HTTP 200). The stream names are now
|
|
// unique-per-invocation (test1_<uniq> etc.) so a POST no longer collides with a
|
|
// prior run's still-deleting "test1/2/3" stream — the local run proved that
|
|
// fixed names returned 400 "stream is being deleted" for ~45s per attempt.
|
|
// This retry now only guards genuine transient cloud errors (connection blip /
|
|
// 5xx); the old single-shot POST swallowed failures, leaving the stream with no
|
|
// data so it never listed and the readiness gate / selectStream timed out.
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logsdata }) => {
|
|
try {
|
|
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(logsdata)
|
|
});
|
|
const contentType = fetchResponse.headers.get('content-type');
|
|
const body = (contentType && contentType.includes('application/json'))
|
|
? await fetchResponse.json()
|
|
: { error: await fetchResponse.text() };
|
|
return { status: fetchResponse.status, body };
|
|
} catch (e) {
|
|
return { status: 0, body: { error: String((e && e.message) || e) } };
|
|
}
|
|
}, {
|
|
url: process.env.INGESTION_URL,
|
|
headers: headers,
|
|
orgId: orgId,
|
|
streamName: testStreamName,
|
|
logsdata: logsdata
|
|
});
|
|
|
|
testLogger.debug('Ingestion response', { attempt, response });
|
|
|
|
if (response.status === 200) {
|
|
return; // accepted — stream will index and become listable
|
|
}
|
|
|
|
const msg = JSON.stringify(response.body || {});
|
|
const retriable = response.status === 0
|
|
|| response.status >= 500
|
|
|| /being deleted|Connection|timeout|ECONNRESET|socket hang up/i.test(msg);
|
|
if (!retriable || attempt === maxRetries) {
|
|
throw new Error(`Ingestion into "${testStreamName}" failed after ${attempt} attempt(s): status=${response.status} body=${msg}`);
|
|
}
|
|
testLogger.warn(`Ingestion into ${testStreamName} not accepted (status ${response.status}) — retrying`, { attempt });
|
|
await page.waitForTimeout(attempt * 2000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Backend-readiness gate: poll the streams list API until the freshly-created
|
|
* stream is actually listed by the backend.
|
|
*
|
|
* WHY: On cloud under load (--workers=5), a just-ingested stream is not
|
|
* immediately visible in the logs stream-list dropdown due to streams-list
|
|
* indexing/consistency lag. logsPage.selectStream() reloads the logs page 5x
|
|
* internally, but if the backend still hasn't listed the stream those retries
|
|
* are exhausted and the test fails with
|
|
* `selectStream: Failed to find stream "..." after N attempts`.
|
|
*
|
|
* This gates on the REAL readiness signal the dropdown itself consumes
|
|
* (GET /api/{org}/streams?type=logs -> body.list contains the stream), so the
|
|
* subsequent selectStream() succeeds deterministically instead of racing.
|
|
* Not a blind retry bump.
|
|
*
|
|
* @param {import('@playwright/test').Page} page
|
|
* @param {string} targetStreamName stream that must appear in the list
|
|
*/
|
|
async function waitForStreamListed(page, targetStreamName) {
|
|
const orgId = getOrgIdentifier();
|
|
// Use the SAME base the app's own stream-readiness check uses
|
|
// (logsPage.waitForStreamAvailable: INGESTION_URL || ZO_BASE_URL) so we poll
|
|
// the exact host/endpoint that feeds selectStream's dropdown. On cloud these
|
|
// two hosts differ, so matching the canonical order avoids hitting the wrong one.
|
|
const rawBase = process.env.INGESTION_URL || process.env.ZO_BASE_URL || '';
|
|
const baseUrl = rawBase.endsWith('/') ? rawBase.slice(0, -1) : rawBase;
|
|
const streamsUrl = `${baseUrl}/api/${orgId}/streams?type=logs`;
|
|
const headers = getAuthHeaders();
|
|
|
|
// Bounded poll: ~60s worst case (20 attempts x 3s backoff), well above the
|
|
// observed consistency lag while still failing fast if the stream is truly
|
|
// never created. Transient network errors are tolerated and retried.
|
|
const maxAttempts = 30; // ~90s: freshly-ingested streams can lag the streams-list under cloud load
|
|
const backoffMs = 3000;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
let listed = false;
|
|
try {
|
|
const response = await page.request.get(streamsUrl, {
|
|
headers,
|
|
timeout: 15000,
|
|
});
|
|
if (response.ok()) {
|
|
const body = await response.json().catch(() => null);
|
|
const list = (body && body.list) || [];
|
|
listed = list.some((s) => s && s.name === targetStreamName);
|
|
} else {
|
|
testLogger.debug('waitForStreamListed non-OK response', {
|
|
status: response.status(),
|
|
targetStreamName,
|
|
attempt,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// Transient Connection timeout / ECONNRESET etc. — retry.
|
|
testLogger.debug('waitForStreamListed request error (will retry)', {
|
|
error: error.message,
|
|
targetStreamName,
|
|
attempt,
|
|
});
|
|
}
|
|
|
|
if (listed) {
|
|
testLogger.info('Stream is listed by backend — safe to selectStream', {
|
|
targetStreamName,
|
|
attempt,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (attempt < maxAttempts) {
|
|
// Documented consistency backoff (streams-list indexing lag on cloud).
|
|
await page.waitForTimeout(backoffMs);
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`waitForStreamListed: stream "${targetStreamName}" was not listed by ` +
|
|
`GET ${streamsUrl} after ${maxAttempts} attempts (~${(maxAttempts * backoffMs) / 1000}s). ` +
|
|
`Backend never made the ingested stream available.`
|
|
);
|
|
}
|
|
|
|
test('stream schema settings updated to be displayed under logs', async ({ page }, testInfo) => {
|
|
const testStreamName = `test1_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
|
|
// Initialize test setup
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
|
|
// Navigate to base URL with authentication
|
|
await navigateToBase(page);
|
|
pm = new PageManager(page);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Perform ingestion for this test
|
|
await ingestion(page, testStreamName);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
// Navigate to logs page with VRL editor enabled
|
|
await pm.logsPage.navigateToLogs(getOrgIdentifier());
|
|
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
|
|
// Backend-readiness gate: ensure the just-ingested stream is actually listed
|
|
// by the streams API before selecting it, avoiding streams-list lag races.
|
|
await waitForStreamListed(page, testStreamName);
|
|
await pm.logsPage.selectStream(testStreamName);
|
|
|
|
const allsearch = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
|
|
await pm.schemaPage.applyQuery();
|
|
await allsearch;
|
|
|
|
// Start of actual test - simplified schema workflow
|
|
await pm.schemaPage.completeStreamSettingsSchemaWorkflow(testStreamName);
|
|
|
|
// Verify schema workflow completed by checking no error messages are present
|
|
await pm.schemaPage.verifyNoErrorMessages();
|
|
testLogger.assertion('Schema workflow completed without errors', true);
|
|
|
|
testLogger.info('Stream schema settings test completed');
|
|
});
|
|
|
|
test('should display stream details on navigating from blank stream to stream with details', async ({ page }, testInfo) => {
|
|
const testStreamName = `test2_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
|
|
// Initialize test setup
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
|
|
// Navigate to base URL with authentication
|
|
await navigateToBase(page);
|
|
pm = new PageManager(page);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Perform ingestion for this test
|
|
await ingestion(page, testStreamName);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
// Navigate to logs page with VRL editor enabled
|
|
await pm.logsPage.navigateToLogs(getOrgIdentifier());
|
|
|
|
// Backend-readiness gate: ensure the just-ingested stream is actually listed
|
|
// by the streams API before selecting it, avoiding streams-list lag races.
|
|
await waitForStreamListed(page, testStreamName);
|
|
await pm.logsPage.selectStream(testStreamName);
|
|
const allsearch2 = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
|
|
await pm.schemaPage.applyQuery();
|
|
await allsearch2;
|
|
|
|
// Start of actual test - complete blank to detailed stream workflow
|
|
await pm.schemaPage.completeBlankToDetailedStreamWorkflow(streamName, testStreamName);
|
|
|
|
// Verify navigation workflow completed by checking streams page is accessible
|
|
await pm.schemaPage.verifyStreamsPageAccessible();
|
|
testLogger.assertion('Stream navigation workflow completed successfully', true);
|
|
});
|
|
|
|
test('should add a new field and delete it from schema', async ({ page }, testInfo) => {
|
|
const testStreamName = `test3_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
|
|
// Initialize test setup
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
|
|
// Navigate to base URL with authentication
|
|
await navigateToBase(page);
|
|
pm = new PageManager(page);
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Perform ingestion for this test
|
|
await ingestion(page, testStreamName);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
// Navigate to logs page with VRL editor enabled
|
|
await pm.logsPage.navigateToLogs(getOrgIdentifier());
|
|
|
|
// Backend-readiness gate: ensure the just-ingested stream is actually listed
|
|
// by the streams API before selecting it, avoiding streams-list lag races.
|
|
await waitForStreamListed(page, testStreamName);
|
|
await pm.logsPage.selectStream(testStreamName);
|
|
const allsearch3 = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
|
|
await pm.schemaPage.applyQuery();
|
|
await allsearch3;
|
|
|
|
// Start of actual test - use original working enhanced framework method
|
|
await pm.schemaPage.completeAddAndDeleteFieldWorkflow(testStreamName);
|
|
|
|
testLogger.assertion('Add/delete field workflow completed successfully', true);
|
|
});
|
|
});
|