Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): quality assurance #874

Merged
merged 17 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,11 @@ DISABLE_RECAPTCHA=false
# It is recommended to use the inivisble type
RECAPTCHA_SECRET_KEY=Recaptcha_Secret_Key
RECAPTCHA_SITE_KEY=Recaptcha_Site_Key

# To turn off quality assurance
# defaults to false
# more info: https://docs.dyrector.io/learn-more/quality-assurance-qa
# QA_OPT_OUT=true

# For providing a group identifier codename for the collected usage data
# QA_GROUP_NAME=
1 change: 1 addition & 0 deletions docker-compose.http-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ services:
- DISABLE_RECAPTCHA=${DISABLE_RECAPTCHA}
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY}
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY}
- QA_OPT_OUT=${QA_OPT_OUT:-false}
depends_on:
crux-postgres:
condition: service_healthy
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ services:
- DISABLE_RECAPTCHA=${DISABLE_RECAPTCHA}
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY}
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY}
- QA_OPT_OUT=${QA_OPT_OUT:-false}
depends_on:
crux-postgres:
condition: service_healthy
Expand Down
4 changes: 3 additions & 1 deletion golang/pkg/cli/container_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ func getCruxEnvs(state *State, args *ArgsFlags) []string {
fmt.Sprintf("FROM_EMAIL=%s", state.SettingsFile.MailFromEmail),
fmt.Sprintf("SMTP_URI=%s:1025/?skip_ssl_verify=true&legacy_ssl=true", state.Containers.MailSlurper.Name),
fmt.Sprintf("AGENT_INSTALL_SCRIPT_DISABLE_PULL=%t", true),
"DISABLE_RECAPTCHA=true")
"DISABLE_RECAPTCHA=true",
"QA_OPT_OUT=true",
)
return append(envs, state.EnvFile...)
}

Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 54 additions & 27 deletions web/crux-ui/e2e/utils/test.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,79 @@ export const test = base.extend({})

export const hookTestPageEvents = (page: Page, testInfo: TestInfo) => {
page.on('console', it => {
const type = it.type()
try {
const type = it.type()

if (!LOG_LEVELS.includes(type)) {
return
}
if (!LOG_LEVELS.includes(type)) {
return
}

const text = it.text()
if (text.includes('Insecure WebSocket connection in production environment!')) {
return
}
const text = it.text()
if (text.includes('Insecure WebSocket connection in production environment!')) {
return
}

console.info(`[${testInfo.title}] ${type.toUpperCase()} ${it.text()}`)
console.info(`[${testInfo.title}] ${type.toUpperCase()} ${it.text()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while writing the console:`, err)
}
})

page.on('request', it => {
if (!it.url().includes('/api/')) {
return
try {
if (!it.url().includes('/api/')) {
return
}

console.info(`[${testInfo.title}] Request started ${it.method()} ${it.url()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while the request started:`, err)
}

console.info(`[${testInfo.title}] Request started ${it.method()} ${it.url()}`)
})

page.on('close', it => {
console.info(`[${testInfo.title}] Page close ${it.url()}`)
try {
console.info(`[${testInfo.title}] Page close ${it.url()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while page close:`, err)
}
})

page.on('requestfailed', it => {
if (!it.url().includes('/api/')) {
return
try {
if (!it.url().includes('/api/')) {
return
}

console.info(`[${testInfo.title}] Request failed ${it.method()} ${it.url()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while request failed:`, err)
}

console.info(`[${testInfo.title}] Request failed ${it.method()} ${it.url()}`)
})

page.on('requestfinished', async it => {
if (!it.url().includes('/api/')) {
return
try {
if (!it.url().includes('/api/')) {
return
}

const res = await it.response()
console.info(`[${testInfo.title}] Request finished ${res.status()} ${it.method()} ${it.url()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while request finished:`, err)
}

const res = await it.response()
console.info(`[${testInfo.title}] Request finished ${res.status()} ${it.method()} ${it.url()}`)
})

page.on('response', it => {
if (!it.url().includes('/api/')) {
return
try {
if (!it.url().includes('/api/')) {
return
}

console.info(`[${testInfo.title}] Response to ${it.url()}`)
} catch (err) {
console.error(`[${testInfo.title}] Error while getting response:`, err)
}

console.info(`[${testInfo.title}] Response to ${it.url()}`)
})
}

Expand Down
18 changes: 18 additions & 0 deletions web/crux-ui/e2e/with-login/project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ test.describe('Project', () => {
})

test.describe('Project versionless filter should work', () => {
const FILTER_VERSIONED = 'filter-versioned-2'
const FILTER_VERSIONLESS = 'filter-versionless-2'

// NOTE(@robot9706): beforeAll runs on each worker, so if tests are running in parallel beforeAll executes multiple times
test.describe.configure({ mode: 'serial' })

test.beforeAll(async ({ browser }, testInfo) => {
const ctx = await browser.newContext()
const page = await ctx.newPage()
hookTestPageEvents(page, testInfo)

await createProject(page, FILTER_VERSIONED, 'versioned')
await createProject(page, FILTER_VERSIONLESS, 'versionless')

await page.close()
await ctx.close()
})

test('in tile view', async ({ page }) => {
await page.goto(TEAM_ROUTES.project.list())
await page.waitForSelector('h2:text-is("Projects")')
Expand Down
27 changes: 27 additions & 0 deletions web/crux-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/crux-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"next": "^13.5.4",
"next-translate": "2.5.2",
"openpgp": "^5.10.1",
"posthog-js": "^1.85.2",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-datepicker": "^4.16.0",
Expand Down
150 changes: 150 additions & 0 deletions web/crux-ui/quality-assurance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { API_QUALITY_ASSURANCE } from '@app/routes'
import { getCruxFromContext } from '@server/crux-api'
import { GetServerSidePropsContext } from 'next'
import posthog from 'posthog-js'
import { ReactHTML } from 'react'

export const QA_URL = 'https://eu.posthog.com'
export const QA_GROUP_TYPE = 'dyoInstance'

export const QA_SETTINGS_PROP = 'qaSettings'
export type QASettings = {
groupId: string
groupName: string
}

export type QAGroupProperties = {
name: string
}

export type QAElementType = keyof Pick<ReactHTML, 'a' | 'div' | 'button' | 'img' | 'select'>
export type QAElementEvent = {
label: string
elementType: QAElementType
}

const QA_EVENT_CLICK = 'click'
export const sendQAClickEvent = (event: QAElementEvent) => posthog.capture(QA_EVENT_CLICK, event)

const QA_EVENT_KEY = 'key'
export type QAKeyEvent = {
label: string
key: string
}

export const sendQAKeyEvent = (event: QAKeyEvent) => posthog.capture(QA_EVENT_KEY, event)

const QA_EVENT_CHECK = 'check'
const QA_EVENT_UNCHECK = 'uncheck'
export const sendQACheckEvent = (event: QAElementEvent, checked: boolean) =>
posthog.capture(checked ? QA_EVENT_CHECK : QA_EVENT_UNCHECK, event)

const QA_EVENT_TOGGLE = 'toggle'
export const sendQAToggleEvent = (label: string) => {
const event: QAElementEvent = {
elementType: 'button',
label,
}

posthog.capture(QA_EVENT_TOGGLE, event)
}

const QA_EVENT_SELECT_RADIO_BUTTON = 'select-radio-button'
export const sendQASelectRadioButtonEvent = (label: string) => {
const event: QAElementEvent = {
elementType: 'div',
label,
}

posthog.capture(QA_EVENT_SELECT_RADIO_BUTTON, event)
}

const QA_EVENT_SELECT_CHIP = 'select-chip'
export const sendQASelectChipEvent = (label: string) => {
const event: QAElementEvent = {
elementType: 'button',
label,
}

posthog.capture(QA_EVENT_SELECT_CHIP, event)
}

const QA_EVENT_SELECT_ICON = 'select-icon'
export const sendQASelectIconEvent = (icon: string) => {
const event: QAElementEvent = {
elementType: 'img',
label: icon,
}

posthog.capture(QA_EVENT_SELECT_ICON, event)
}

const QA_EVENT_DIALOG = 'dialog'
type DialogEventType = 'open' | 'close'
export const sendQADialogEvent = (label: string, eventType: DialogEventType) => {
const event: QAElementEvent = {
elementType: 'div',
label,
}

posthog.capture(`${QA_EVENT_DIALOG}-${eventType}`, event)
}

export const QA_DIALOG_LABEL_DELETE_CONFIG_BUNDLE = 'deleteConfigBundle'
export const QA_DIALOG_LABEL_REVOKE_NODE_TOKEN = 'revokeNodeToken'
export const QA_DIALOG_LABEL_KICK_AGENT = 'kickAgent'
export const QA_DIALOG_LABEL_DELETE_CONTAINER = 'deleteContainer'
export const QA_DIALOG_LABEL_SET_AS_DEFAULT = 'setAsDefault'
export const QA_DIALOG_LABEL_DEPLOY = 'deploy'
export const QA_DIALOG_LABEL_DELETE_DEPLOYMENT = 'deleteDeployment'
export const QA_DIALOG_LABEL_DELETE_IMAGE = 'deleteImage'
export const QA_DIALOG_LABEL_REVOKE_DEPLOY_TOKEN = 'revokeDeployToken'
export const QA_DIALOG_LABEL_DELETE_FROM_PAGE_MENU = 'deleteFromPageMenu'
export const QA_DIALOG_LABEL_CHANGE_TEAM_SLUG = 'changeTeamSlug'
export const QA_DIALOG_LABEL_LEAVE_TEAM = 'leaveTeam'
export const QA_DIALOG_LABEL_UPDATE_USER_ROLE = 'updateUserRole'
export const QA_DIALOG_LABEL_DEPLOY_PROTECTED = 'deployProtected'
export const QA_DIALOG_LABEL_HIDE_ONBOARDING = 'hideOnboarding'
export const QA_DIALOG_LABEL_CONVERT_PROJECT_TO_VERSIONED = ''
export const QA_DIALOG_LABEL_REMOVE_OIDC_ACCOUNT = 'removeOIDCAccount'
export const QA_DIALOG_LABEL_DELETE_USER_TOKEN = 'deleteUserToken'
export const QA_DIALOG_LABEL_DELETE_USER = 'deleteUser'

export const QA_MODAL_LABEL_NODE_AUDIT_DETAILS = 'nodeAuditDetails'
export const QA_MODAL_LABEL_DEPLOYMENT_NOTE = 'deploymentNote'
export const QA_MODAL_LABEL_IMAGE_TAGS = 'image-tags'
export const QA_MODAL_LABEL_AUDIT_LOG_DETAILS = 'auditLogDetails'

export const QA_LINK_LABEL = ''

export type QualityAssurance = {
disabled: boolean
group?: {
id: string
name: string
}
}

type QAAwareHttpServer = {
qa: QualityAssurance
}

export const fetchQualityAssuranceSettings = async (context: GetServerSidePropsContext): Promise<QASettings | null> => {
const httpServer: QAAwareHttpServer = (context.req.socket as any).server
if (!httpServer.qa) {
httpServer.qa = await getCruxFromContext<QualityAssurance>(context, API_QUALITY_ASSURANCE)
}

const { qa } = httpServer

if (!qa || !qa.group) {
return null
}

const { id, name } = qa.group

return {
groupId: id,
groupName: name,
}
}
Loading
Loading