diff --git a/.eslintrc.json b/.eslintrc.json index 82f6a122d..9cbbd586a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,7 @@ "browser": true, "es2020": true, "node": true, + "jquery": true, "jest": true }, "parser": "@typescript-eslint/parser", @@ -14,6 +15,7 @@ "sourceType": "module" }, "extends": [ + "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "eslint:recommended", "plugin:prettier/recommended", @@ -33,12 +35,14 @@ "react/prop-types": "off", "import/no-anonymous-default-export": "off", "import/no-named-as-default": "off", + "css-modules/no-unused-class": "off", "@next/next/no-img-element": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], + "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }] }, "globals": { "React": "writable" diff --git a/cypress/e2e/api.cy.ts b/cypress/e2e/api.cy.ts new file mode 100644 index 000000000..e69b5dff6 --- /dev/null +++ b/cypress/e2e/api.cy.ts @@ -0,0 +1,29 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + }); + + //let userId; + + it('creates a user.', () => { + cy.fixture('users').then(data => { + const userPost = data.userPost; + cy.request({ + method: 'POST', + url: '/api/users', + headers: { + 'Content-Type': 'application/json', + Authorization: Cypress.env('authorization'), + }, + body: userPost, + }).then(response => { + //userId = response.body.id; + expect(response.status).to.eq(200); + expect(response.body).to.have.property('username', 'cypress1'); + expect(response.body).to.have.property('role', 'User'); + }); + }); + }); +}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 5831c81d6..507b1b580 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,22 +1,36 @@ describe('Login tests', () => { + beforeEach(() => { + cy.visit('/login'); + }); + it( 'logs user in with correct credentials and logs user out', { defaultCommandTimeout: 10000, }, () => { - cy.visit('/login'); - cy.getDataTest('input-username').find('input').click(); - cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 }); - cy.getDataTest('input-password').find('input').click(); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); cy.getDataTest('input-password') .find('input') - .type(Cypress.env('umami_password'), { delay: 50 }); + .type(Cypress.env('umami_password'), { delay: 0 }); cy.getDataTest('button-submit').click(); cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); - cy.getDataTest('button-profile').click(); - cy.getDataTest('item-logout').click(); - cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.logout(); }, ); + + it('login with blank inputs or incorrect credentials', () => { + cy.getDataTest('button-submit').click(); + cy.contains(/Required/i).should('be.visible'); + + cy.getDataTest('input-username').find('input').as('inputUsername'); + cy.get('@inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.contains(/Incorrect username and\/or password./i).should('be.visible'); + }); }); diff --git a/cypress/e2e/user.cy.ts b/cypress/e2e/user.cy.ts new file mode 100644 index 000000000..9f432f16a --- /dev/null +++ b/cypress/e2e/user.cy.ts @@ -0,0 +1,65 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + cy.visit('/settings/users'); + }); + + it('Add a User', () => { + // add user + cy.contains(/Create user/i).should('be.visible'); + cy.getDataTest('button-create-user').click(); + cy.getDataTest('input-username').find('input').as('inputName').click(); + cy.get('@inputName').type('Test-user', { delay: 0 }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-user').click(); + cy.getDataTest('button-submit').click(); + cy.get('td[label="Username"]').should('contain.text', 'Test-user'); + cy.get('td[label="Role"]').should('contain.text', 'User'); + }); + + it('Edit a User role and password', () => { + // edit user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row + }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('newPassword', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-viewOnly').click(); + cy.getDataTest('button-submit').click(); + + cy.visit('/settings/users'); + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .should('contain.text', 'View only'); + + cy.logout(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type('Test-user', { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); + }); + + it('Delete a website', () => { + // delete user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('button-delete').click(); // Clicks the button inside the row + }); + cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible'); + cy.getDataTest('button-confirm').click(); + }); +}); diff --git a/cypress/e2e/website.cy.ts b/cypress/e2e/website.cy.ts index b60d8e7a4..2dcd60273 100644 --- a/cypress/e2e/website.cy.ts +++ b/cypress/e2e/website.cy.ts @@ -10,10 +10,10 @@ describe('Website tests', () => { cy.visit('/settings/websites'); cy.getDataTest('button-website-add').click(); cy.contains(/Add website/i).should('be.visible'); - cy.getDataTest('input-name').find('input').click(); - cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 }); + cy.getDataTest('input-name').find('input').as('inputUsername').click(); + cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); - cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 }); cy.getDataTest('button-submit').click(); cy.get('td[label="Name"]').should('contain.text', 'Add test'); cy.get('td[label="Domain"]').should('contain.text', 'addtest.com'); @@ -41,10 +41,10 @@ describe('Website tests', () => { cy.contains(/Details/i).should('be.visible'); cy.getDataTest('input-name').find('input').click(); cy.getDataTest('input-name').find('input').clear(); - cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 }); + cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); cy.getDataTest('input-domain').find('input').clear(); - cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 }); cy.getDataTest('button-submit').click({ force: true }); cy.getDataTest('input-name').find('input').should('have.value', 'Updated website'); cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com'); diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json new file mode 100644 index 000000000..420a71c39 --- /dev/null +++ b/cypress/fixtures/users.json @@ -0,0 +1,17 @@ +{ + "userGet": { + "name": "cypress", + "email": "password", + "role": "User" + }, + "userPost": { + "username": "cypress1", + "password": "password", + "role": "User" + }, + "userDelete": { + "name": "Charlie", + "email": "charlie@example.com", + "age": 35 + } +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2c45142b3..a300b9691 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => { return cy.get(`[data-test=${value}]`); }); +Cypress.Commands.add('logout', () => { + cy.getDataTest('button-profile').click(); + cy.getDataTest('item-logout').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); +}); + Cypress.Commands.add('login', (username: string, password: string) => { cy.session([username, password], () => { cy.request({ diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 90cca19b2..e89b24dd8 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -1,4 +1,5 @@ /// +/* global JQuery */ declare namespace Cypress { interface Chainable { @@ -7,6 +8,11 @@ declare namespace Cypress { * @example cy.getDataTest('greeting') */ getDataTest(value: string): Chainable>; + /** + * Custom command to logout through UI. + * @example cy.logout() + */ + logout(): Chainable>; /** * Custom command to login user into the app. * @example cy.login('admin', 'password) diff --git a/next.config.js b/next.config.js index 7a65c4727..590d7121b 100644 --- a/next.config.js +++ b/next.config.js @@ -59,15 +59,29 @@ const trackerHeaders = [ }, ]; +const apiHeaders = [ + { + key: 'Access-Control-Allow-Origin', + value: '*' + }, + { + key: 'Access-Control-Allow-Headers', + value: '*' + }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET, DELETE, POST, PUT' + }, + { + key: 'Access-Control-Max-Age', + value: corsMaxAge || '86400' + }, +]; + const headers = [ { source: '/api/:path*', - headers: [ - { key: 'Access-Control-Allow-Origin', value: '*' }, - { key: 'Access-Control-Allow-Headers', value: '*' }, - { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, - { key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' }, - ], + headers: apiHeaders }, { source: '/:path*', @@ -89,6 +103,11 @@ if (trackerScriptURL) { } if (collectApiEndpoint) { + headers.push({ + source: collectApiEndpoint, + headers: apiHeaders, + }); + rewrites.push({ source: collectApiEndpoint, destination: '/api/send', diff --git a/package.json b/package.json index 275f1408f..db146e9a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.16.0", + "version": "2.17.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index c6bb04a28..b0fd0239f 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Kode akses" } ], "label.actions": [ @@ -14,31 +14,31 @@ "label.activity": [ { "type": 0, - "value": "Activity log" + "value": "Catatan aktivitas" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "Tambah" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "Tambah deskripsi" } ], "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "Tambah anggota" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "Tambah langkah" } ], "label.add-website": [ @@ -56,7 +56,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "Setelah" } ], "label.all": [ @@ -74,13 +74,13 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Analitik" } ], "label.average": [ { "type": 0, - "value": "Average" + "value": "Rata-rata" } ], "label.back": [ @@ -92,7 +92,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Sebelum" } ], "label.bounce-rate": [ @@ -104,13 +104,13 @@ "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "Rincian" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Peramban" } ], "label.browsers": [ @@ -134,31 +134,31 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Kota" } ], "label.city": [ { "type": 0, - "value": "City" + "value": "Kota" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Hapus semua" } ], "label.compare": [ { "type": 0, - "value": "Compare" + "value": "Bandingkan" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Konfirmasi" } ], "label.confirm-password": [ @@ -170,19 +170,19 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Mengandung" } ], "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Lanjutkan" } ], "label.count": [ { "type": 0, - "value": "Count" + "value": "Jumlah" } ], "label.countries": [ @@ -194,49 +194,49 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Negara" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "Buat" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "Buat laporan" } ], "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Buat tim" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Buat pengguna" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Dibuat" } ], "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "Dibuat oleh" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "Saat ini" } ], "label.current-password": [ @@ -266,7 +266,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "Tanggal" } ], "label.date-range": [ @@ -278,7 +278,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Hari" } ], "label.default-date-range": [ @@ -296,19 +296,19 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "Hapus laporan" } ], "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Hapus tim" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Hapus pengguna" } ], "label.delete-website": [ @@ -320,7 +320,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "Deskripsi" } ], "label.desktop": [ @@ -332,13 +332,13 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Detail" } ], "label.device": [ { "type": 0, - "value": "Device" + "value": "Perangkat" } ], "label.devices": [ @@ -356,7 +356,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Tidak mengandung" } ], "label.domain": [ @@ -368,7 +368,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Penurunan" } ], "label.edit": [ @@ -380,13 +380,13 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Sunting dasbor" } ], "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "Sunting anggota" } ], "label.enable-share-url": [ @@ -398,31 +398,31 @@ "label.end-step": [ { "type": 0, - "value": "End Step" + "value": "Langkah akhir" } ], "label.entry": [ { "type": 0, - "value": "Entry URL" + "value": "URL masuk" } ], "label.event": [ { "type": 0, - "value": "Event" + "value": "Peristiwa" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "Data peristiwa" } ], "label.events": [ { "type": 0, - "value": "Perihal" + "value": "Peristiwa" } ], "label.exit": [ @@ -434,19 +434,19 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Salah" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Kolom" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Kolom" } ], "label.filter": [ @@ -476,7 +476,7 @@ "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "Pertama kali dilihat" } ], "label.funnel": [ @@ -488,37 +488,37 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "Pahami tingkat konversi dan penurunan pengguna." } ], "label.goal": [ { "type": 0, - "value": "Goal" + "value": "Tujuan" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "Tujuan" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "Lacak tujuan Anda untuk tampilan halaman dan peristiwa." } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "Lebih dari" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "Lebih dari atau sama dengan" } ], "label.host": [ @@ -536,61 +536,61 @@ "label.insights": [ { "type": 0, - "value": "Insights" + "value": "Wawasan" } ], "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "Jelajahi data Anda lebih dalam dengan menggunakan segmen dan filter." } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "Adalah" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Bukan" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Tidak diatur" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Diatur" } ], "label.join": [ { "type": 0, - "value": "Join" + "value": "Gabung" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Gabung tim" } ], "label.journey": [ { "type": 0, - "value": "Journey" + "value": "Perjalanan" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "Pahami bagaimana pengguna menavigasi situs web Anda." } ], "label.language": [ @@ -632,47 +632,43 @@ } ], "label.last-months": [ - { - "type": 0, - "value": "Last " - }, { "type": 1, "value": "x" }, { "type": 0, - "value": " months" + "value": " bulan terakhir" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "Terakhir kali dilihat" } ], "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Keluar" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Keluar dari tim" } ], "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "Kurang dari" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "Kurang dari atau sama dengan" } ], "label.login": [ @@ -690,31 +686,31 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "Kelola" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "Pengelola" } ], "label.max": [ { "type": 0, - "value": "Max" + "value": "Maks" } ], "label.member": [ { "type": 0, - "value": "Member" + "value": "Anggota" } ], "label.members": [ { "type": 0, - "value": "Members" + "value": "Anggota" } ], "label.min": [ @@ -738,13 +734,13 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "Akun saya" } ], "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Situs web saya" } ], "label.name": [ @@ -762,7 +758,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "Tidak ada" } ], "label.number-of-records": [ @@ -814,7 +810,7 @@ "label.overview": [ { "type": 0, - "value": "Overview" + "value": "Tinjauan umum" } ], "label.owner": [ @@ -826,7 +822,7 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Halaman " }, { "type": 1, @@ -834,7 +830,7 @@ }, { "type": 0, - "value": " of " + "value": " dari " }, { "type": 1, @@ -850,7 +846,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Judul halaman" } ], "label.pages": [ @@ -890,19 +886,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "Sebelumnya" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "Periode sebelumnya" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "Tahun lalu" } ], "label.profile": [ @@ -950,7 +946,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Perujuk" } ], "label.referrers": [ @@ -968,37 +964,37 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Buat ulang" } ], "label.region": [ { "type": 0, - "value": "Region" + "value": "Wilayah" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Wilayah" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Hapus" } ], "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "Hapus anggota" } ], "label.reports": [ { "type": 0, - "value": "Reports" + "value": "Laporan" } ], "label.required": [ @@ -1022,31 +1018,31 @@ "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Retensi" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "Ukur daya tarik situs web Anda dengan melacak seberapa sering pengguna kembali." } ], "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "Pendapatan" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "Lihat pendapatan Anda seiring waktu." } ], "label.revenue-property": [ { "type": 0, - "value": "Revenue Property" + "value": "Properti pendapatan" } ], "label.role": [ @@ -1076,43 +1072,43 @@ "label.search": [ { "type": 0, - "value": "Search" + "value": "Cari" } ], "label.select": [ { "type": 0, - "value": "Select" + "value": "Pilih" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "Pilih tanggal" } ], "label.select-role": [ { "type": 0, - "value": "Select role" + "value": "Pilih role" } ], "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Pilih situs web" } ], "label.session": [ { "type": 0, - "value": "Session" + "value": "Sesi" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sesi" } ], "label.settings": [ @@ -1136,13 +1132,13 @@ "label.start-step": [ { "type": 0, - "value": "Start Step" + "value": "Langkah awal" } ], "label.steps": [ { "type": 0, - "value": "Steps" + "value": "Langkah" } ], "label.sum": [ @@ -1160,37 +1156,37 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Tim" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "ID tim" } ], "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "Pengelola tim" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Anggota tim" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Nama tim" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Pemilik tim" } ], "label.team-view-only": [ @@ -1202,13 +1198,13 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Situs web tim" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Tim" } ], "label.theme": [ @@ -1244,7 +1240,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Judul" } ], "label.today": [ @@ -1268,7 +1264,7 @@ "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Total baris" } ], "label.tracking-code": [ @@ -1280,7 +1276,7 @@ "label.transactions": [ { "type": 0, - "value": "Transactions" + "value": "Transaksi" } ], "label.transfer": [ @@ -1292,25 +1288,25 @@ "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "Transfer situs web" } ], "label.true": [ { "type": 0, - "value": "True" + "value": "Benar" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Tipe" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "Unik" } ], "label.unique-visitors": [ @@ -1322,7 +1318,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "Kustomer unik" } ], "label.unknown": [ @@ -1334,13 +1330,13 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Tanpa judul" } ], "label.update": [ { "type": 0, - "value": "Update" + "value": "Perbarui" } ], "label.url": [ @@ -1358,7 +1354,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Pengguna" } ], "label.user-property": [ @@ -1376,7 +1372,7 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Pengguna" } ], "label.utm": [ @@ -1388,19 +1384,19 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "Lacak kampanye Anda melalui parameter UTM." } ], "label.value": [ { "type": 0, - "value": "Value" + "value": "Nilai" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Lihat" } ], "label.view-details": [ @@ -1412,7 +1408,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Hanya melihat" } ], "label.views": [ @@ -1424,7 +1420,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "Tampilan per kunjungan" } ], "label.visit-duration": [ @@ -1442,19 +1438,19 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "Kunjungan" } ], "label.website": [ { "type": 0, - "value": "Website" + "value": "Situs web" } ], "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "ID situs web" } ], "label.websites": [ @@ -1472,13 +1468,13 @@ "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Kemarin" } ], "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "Ketik " }, { "type": 1, @@ -1486,7 +1482,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " pada kotak di bawah untuk mengonfirmasi." } ], "message.active-users": [ @@ -1502,7 +1498,7 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "Data dikumpulkan" } ], "message.confirm-delete": [ @@ -1522,7 +1518,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Apakah Anda yakin ingin meninggalkan " }, { "type": 1, @@ -1536,7 +1532,7 @@ "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "Apakah Anda yakin ingin menghapus " }, { "type": 1, @@ -1564,7 +1560,7 @@ "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "Menghapus tim juga akan menghapus semua situs web yang terkait." } ], "message.delete-website-warning": [ @@ -1614,7 +1610,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Minimal " }, { "type": 1, @@ -1622,13 +1618,13 @@ }, { "type": 0, - "value": " characters" + "value": " karakter" } ], "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Versi baru dari Umami " }, { "type": 1, @@ -1636,7 +1632,7 @@ }, { "type": 0, - "value": " is available!" + "value": " telah tersedia!" } ], "message.no-data-available": [ @@ -1648,7 +1644,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Tidak ada data peristiwa" } ], "message.no-match-password": [ @@ -1660,25 +1656,25 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "Tidak ada hasil yang ditemukan." } ], "message.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Tim ini tidak memiliki situs web." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Anda belum membuat tim." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Tidak ada pengguna." } ], "message.no-websites-configured": [ @@ -1696,7 +1692,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Untuk mengatur ulang situs web ini, ketik " }, { "type": 1, @@ -1704,13 +1700,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " pada kotak di bawah untuk mengonfirmasi." } ], "message.reset-website-warning": [ { "type": 0, - "value": "Semua statistik pada website ini akan dihapus, tetapi kode lacak akan tetap terpasang" + "value": "Semua statistik pada situs web ini akan dihapus, tetapi kode lacak akan tetap terpasang" } ], "message.saved": [ @@ -1736,19 +1732,19 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Anda sudah menjadi anggota tim ini." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Tim tidak ditemukan." } ], "message.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Situs web dapat dilihat oleh semua anggota tim." } ], "message.tracking-code": [ @@ -1760,37 +1756,37 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "Transfer situs web ini ke akun Anda?" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "Pilih tim tujuan untuk mentransfer situs web ini." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "Transfer kepemilikan situs web ke akun Anda atau tim lain" } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "Peristiwa terjadi" } ], "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Pengguna telah dihapus." } ], "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "Halaman dilihat" } ], "message.visitor-log": [ @@ -1830,7 +1826,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "Pengunjung yang meninggalkan situs web" } ] } diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index f1a76b9b8..85527896f 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -32,13 +32,13 @@ "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "Гишүүн нэмэх" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "Алхам нэмэх" } ], "label.add-website": [ @@ -74,7 +74,7 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Аналитик" } ], "label.average": [ @@ -152,7 +152,7 @@ "label.compare": [ { "type": 0, - "value": "Compare" + "value": "Харьцуулах" } ], "label.confirm": [ @@ -182,7 +182,7 @@ "label.count": [ { "type": 0, - "value": "Count" + "value": "Тоо" } ], "label.countries": [ @@ -230,13 +230,13 @@ "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "Үүсгэсэн" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "Одоогийн" } ], "label.current-password": [ @@ -296,7 +296,7 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "Тайлан устгах" } ], "label.delete-team": [ @@ -386,7 +386,7 @@ "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "Гишүүн засах" } ], "label.enable-share-url": [ @@ -398,13 +398,13 @@ "label.end-step": [ { "type": 0, - "value": "End Step" + "value": "Төгсгөлийн алхам" } ], "label.entry": [ { "type": 0, - "value": "Entry URL" + "value": "Орох зам" } ], "label.event": [ @@ -428,7 +428,7 @@ "label.exit": [ { "type": 0, - "value": "Exit URL" + "value": "Гарах зам" } ], "label.false": [ @@ -476,7 +476,7 @@ "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "Анх харсан" } ], "label.funnel": [ @@ -494,19 +494,19 @@ "label.goal": [ { "type": 0, - "value": "Goal" + "value": "Зорилго" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "Зорилго" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "Хуудас үзсэн болон үйлдлийн зорилгыг мөрдөх." } ], "label.greater-than": [ @@ -524,13 +524,13 @@ "label.host": [ { "type": 0, - "value": "Host" + "value": "Хост" } ], "label.hosts": [ { "type": 0, - "value": "Hosts" + "value": "Хост" } ], "label.insights": [ @@ -584,13 +584,13 @@ "label.journey": [ { "type": 0, - "value": "Journey" + "value": "Аялал" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "Хэрэглэгчид таны цахим хуудсаар хэрхэн шилжиж явсныг шинжлэх." } ], "label.language": [ @@ -642,7 +642,7 @@ "label.last-months": [ { "type": 0, - "value": "Last " + "value": "Сүүлийн " }, { "type": 1, @@ -650,13 +650,13 @@ }, { "type": 0, - "value": " months" + "value": " сар" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "Сүүлд харагдсан" } ], "label.leave": [ @@ -698,13 +698,13 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "Удирдах" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "Удирдагч" } ], "label.max": [ @@ -716,7 +716,7 @@ "label.member": [ { "type": 0, - "value": "Member" + "value": "Гишүүн" } ], "label.members": [ @@ -746,7 +746,7 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "Миний бүртгэл" } ], "label.my-websites": [ @@ -789,7 +789,7 @@ "value": [ { "type": 0, - "value": "record" + "value": "бичлэг" } ] }, @@ -797,7 +797,7 @@ "value": [ { "type": 0, - "value": "records" + "value": "бичлэг" } ] } @@ -810,7 +810,7 @@ "label.ok": [ { "type": 0, - "value": "OK" + "value": "ЗА" } ], "label.os": [ @@ -876,13 +876,13 @@ "label.path": [ { "type": 0, - "value": "Path" + "value": "Зам" } ], "label.paths": [ { "type": 0, - "value": "Paths" + "value": "Зам" } ], "label.powered-by": [ @@ -898,19 +898,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "Өмнөх" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "Өмнөх үе" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "Өмнөх жил" } ], "label.profile": [ @@ -922,13 +922,13 @@ "label.properties": [ { "type": 0, - "value": "Properties" + "value": "Шинж чанар" } ], "label.property": [ { "type": 0, - "value": "Property" + "value": "Шинж чанар" } ], "label.queries": [ @@ -1000,7 +1000,7 @@ "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "Гишүүн хасах" } ], "label.reports": [ @@ -1042,19 +1042,19 @@ "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "Орлого" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "Цаг хугацааны туршид орлогын өөрчлөлтийг харах." } ], "label.revenue-property": [ { "type": 0, - "value": "Revenue Property" + "value": "Орлогын шинж чанар" } ], "label.role": [ @@ -1090,7 +1090,7 @@ "label.select": [ { "type": 0, - "value": "Select" + "value": "Сонгох" } ], "label.select-date": [ @@ -1144,13 +1144,13 @@ "label.start-step": [ { "type": 0, - "value": "Start Step" + "value": "Эхлэх алхам" } ], "label.steps": [ { "type": 0, - "value": "Steps" + "value": "Алхам" } ], "label.sum": [ @@ -1180,7 +1180,7 @@ "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "Багийн удирдагч" } ], "label.team-member": [ @@ -1294,13 +1294,13 @@ "label.transfer": [ { "type": 0, - "value": "Transfer" + "value": "Шилжүүлэх" } ], "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "Вебийг шилжүүлэх" } ], "label.true": [ @@ -1330,7 +1330,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "Давтагдаагүй зочин" } ], "label.unknown": [ @@ -1348,7 +1348,7 @@ "label.update": [ { "type": 0, - "value": "Update" + "value": "Шинэчлэх" } ], "label.url": [ @@ -1360,7 +1360,7 @@ "label.urls": [ { "type": 0, - "value": "URLs" + "value": "URL-ууд" } ], "label.user": [ @@ -1372,7 +1372,7 @@ "label.user-property": [ { "type": 0, - "value": "User Property" + "value": "Хэрэглэгчийн шинж" } ], "label.username": [ @@ -1396,7 +1396,7 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "UTM параметраар кампанит ажлаа мөрдөх." } ], "label.value": [ @@ -1432,7 +1432,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "Зочдын хуудас үзсэн тоо" } ], "label.visit-duration": [ @@ -1450,7 +1450,7 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "Зочилсон" } ], "label.website": [ @@ -1486,7 +1486,7 @@ "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "Доорх хэсэгт " }, { "type": 1, @@ -1494,7 +1494,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " гэж бичин баталгаажуулна уу." } ], "message.active-users": [ @@ -1542,7 +1542,7 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "Цуглуулсан өгөгдөл" } ], "message.confirm-delete": [ @@ -1576,7 +1576,7 @@ "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "Та " }, { "type": 1, @@ -1584,7 +1584,7 @@ }, { "type": 0, - "value": "?" + "value": "-г устгахдаа итгэлтэй байна уу?" } ], "message.confirm-reset": [ @@ -1604,7 +1604,7 @@ "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "Баг устгах нь мөн түүнд харъяалагдах вебүүдийг устгах болно." } ], "message.delete-website-warning": [ @@ -1806,25 +1806,25 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "Энэ вебийг өөрийн бүртгэл рүү шилжүүлэх үү?" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "Энэ вебийг шилжүүлж авах багийг сонгоно уу." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "Энэ вебийг өөрийн бүртгэл рүү эсвэл багт шилжүүлж авах." } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "Өдөөсөн үйлдэл" } ], "message.user-deleted": [ @@ -1836,7 +1836,7 @@ "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "Үзсэн хуудас" } ], "message.visitor-log": [ @@ -1876,7 +1876,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "Зочдын уналт" } ] } diff --git a/scripts/check-db.js b/scripts/check-db.js index cdfeafa32..ca0fca31c 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -82,9 +82,11 @@ async function checkV1Tables() { } async function applyMigration() { - console.log(execSync('prisma migrate deploy').toString()); + if (!process.env.SKIP_DB_MIGRATION) { + console.log(execSync('prisma migrate deploy').toString()); - success('Database is up to date.'); + success('Database is up to date.'); + } } (async () => { diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index efd1da3c7..ca096675f 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -39,7 +39,9 @@ export function ReportDeleteButton({ {(close: () => void) => ( {reportName} })} + message={formatMessage(messages.confirmDelete, { + target: {reportName}, + })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.tsx b/src/app/(main)/settings/teams/TeamLeaveForm.tsx index daf464341..389ba4ea7 100644 --- a/src/app/(main)/settings/teams/TeamLeaveForm.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveForm.tsx @@ -34,7 +34,9 @@ export function TeamLeaveForm({ return ( {teamName} })} + message={formatMessage(messages.confirmLeave, { + target: {teamName}, + })} onConfirm={handleConfirm} onClose={onClose} isLoading={isPending} diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx index e1b048420..674771b6e 100644 --- a/src/app/(main)/settings/users/UserAddButton.tsx +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -15,7 +15,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) { return ( - diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx index baf5949f2..9ef5b30a0 100644 --- a/src/components/common/TypeConfirmationForm.tsx +++ b/src/components/common/TypeConfirmationForm.tsx @@ -35,7 +35,9 @@ export function TypeConfirmationForm({ return (

- {formatMessage(messages.actionConfirmation, { confirmation: {confirmationValue} })} + {formatMessage(messages.actionConfirmation, { + confirmation: {confirmationValue}, + })}

value === confirmationValue }}> diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index db40a6177..4d5a87c36 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -69,11 +69,10 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { if (!groups[domain]) { groups[domain] = 0; } - groups[domain] += y; - } else { - groups._other += y; + groups[domain] += +y; } } + groups._other += +y; } return Object.keys(groups) diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index 6cf7659ca..b6eb83ce1 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -1,145 +1,145 @@ { - "label.access-code": "Access code", + "label.access-code": "Kode akses", "label.actions": "Aksi", - "label.activity": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.activity": "Catatan aktivitas", + "label.add": "Tambah", + "label.add-description": "Tambah deskripsi", + "label.add-member": "Tambah anggota", + "label.add-step": "Tambah langkah", "label.add-website": "Tambah situs web", "label.admin": "Pengelola", - "label.after": "After", + "label.after": "Setelah", "label.all": "Semua", "label.all-time": "Semua waktu", - "label.analytics": "Analytics", - "label.average": "Average", + "label.analytics": "Analitik", + "label.average": "Rata-rata", "label.back": "Kembali", - "label.before": "Before", + "label.before": "Sebelum", "label.bounce-rate": "Rasio pentalan", - "label.breakdown": "Breakdown", - "label.browser": "Browser", + "label.breakdown": "Rincian", + "label.browser": "Peramban", "label.browsers": "Peramban", "label.cancel": "Batal", "label.change-password": "Ganti kata sandi", - "label.cities": "Cities", - "label.city": "City", - "label.clear-all": "Clear all", - "label.compare": "Compare", - "label.confirm": "Confirm", + "label.cities": "Kota", + "label.city": "Kota", + "label.clear-all": "Hapus semua", + "label.compare": "Bandingkan", + "label.confirm": "Konfirmasi", "label.confirm-password": "Konfirmasi kata sandi", - "label.contains": "Contains", - "label.continue": "Continue", - "label.count": "Count", + "label.contains": "Mengandung", + "label.continue": "Lanjutkan", + "label.count": "Jumlah", "label.countries": "Negara", - "label.country": "Country", - "label.create": "Create", - "label.create-report": "Create report", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", - "label.created-by": "Created By", - "label.current": "Current", + "label.country": "Negara", + "label.create": "Buat", + "label.create-report": "Buat laporan", + "label.create-team": "Buat tim", + "label.create-user": "Buat pengguna", + "label.created": "Dibuat", + "label.created-by": "Dibuat oleh", + "label.current": "Saat ini", "label.current-password": "Kata sandi sekarang", "label.custom-range": "Rentang khusus", "label.dashboard": "Dasbor", "label.data": "Data", - "label.date": "Date", + "label.date": "Tanggal", "label.date-range": "Rentang tanggal", - "label.day": "Day", + "label.day": "Hari", "label.default-date-range": "Rentang tanggal bawaan", "label.delete": "Hapus", - "label.delete-report": "Delete report", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-report": "Hapus laporan", + "label.delete-team": "Hapus tim", + "label.delete-user": "Hapus pengguna", "label.delete-website": "Hapus situs web", - "label.description": "Description", + "label.description": "Deskripsi", "label.desktop": "Desktop", - "label.details": "Details", - "label.device": "Device", + "label.details": "Detail", + "label.device": "Perangkat", "label.devices": "Perangkat", "label.dismiss": "Tutup", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "Tidak mengandung", "label.domain": "Domain", - "label.dropoff": "Dropoff", + "label.dropoff": "Penurunan", "label.edit": "Sunting", - "label.edit-dashboard": "Edit dashboard", - "label.edit-member": "Edit member", + "label.edit-dashboard": "Sunting dasbor", + "label.edit-member": "Sunting anggota", "label.enable-share-url": "Aktifkan URL berbagi", - "label.end-step": "End Step", - "label.entry": "Entry URL", - "label.event": "Event", - "label.event-data": "Event data", - "label.events": "Perihal", + "label.end-step": "Langkah akhir", + "label.entry": "URL masuk", + "label.event": "Peristiwa", + "label.event-data": "Data peristiwa", + "label.events": "Peristiwa", "label.exit": "Exit URL", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", + "label.false": "Salah", + "label.field": "Kolom", + "label.fields": "Kolom", "label.filter": "Filter", "label.filter-combined": "Gabungan", "label.filter-raw": "Mentah", "label.filters": "Filters", - "label.first-seen": "First seen", + "label.first-seen": "Pertama kali dilihat", "label.funnel": "Funnel", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", - "label.goal": "Goal", - "label.goals": "Goals", - "label.goals-description": "Track your goals for pageviews and events.", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", + "label.funnel-description": "Pahami tingkat konversi dan penurunan pengguna.", + "label.goal": "Tujuan", + "label.goals": "Tujuan", + "label.goals-description": "Lacak tujuan Anda untuk tampilan halaman dan peristiwa.", + "label.greater-than": "Lebih dari", + "label.greater-than-equals": "Lebih dari atau sama dengan", "label.host": "Host", "label.hosts": "Hosts", - "label.insights": "Insights", - "label.insights-description": "Dive deeper into your data by using segments and filters.", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", - "label.join": "Join", - "label.join-team": "Join team", - "label.journey": "Journey", - "label.journey-description": "Understand how users navigate through your website.", + "label.insights": "Wawasan", + "label.insights-description": "Jelajahi data Anda lebih dalam dengan menggunakan segmen dan filter.", + "label.is": "Adalah", + "label.is-not": "Bukan", + "label.is-not-set": "Tidak diatur", + "label.is-set": "Diatur", + "label.join": "Gabung", + "label.join-team": "Gabung tim", + "label.journey": "Perjalanan", + "label.journey-description": "Pahami bagaimana pengguna menavigasi situs web Anda.", "label.language": "Bahasa", "label.languages": "Bahasa", "label.laptop": "Laptop", "label.last-days": "{x} hari terakhir", "label.last-hours": "{x} jam terakhir", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.last-months": "{x} bulan terakhir", + "label.last-seen": "Terakhir kali dilihat", + "label.leave": "Keluar", + "label.leave-team": "Keluar dari tim", + "label.less-than": "Kurang dari", + "label.less-than-equals": "Kurang dari atau sama dengan", "label.login": "Masuk", "label.logout": "Keluar", - "label.manage": "Manage", - "label.manager": "Manager", - "label.max": "Max", - "label.member": "Member", - "label.members": "Members", + "label.manage": "Kelola", + "label.manager": "Pengelola", + "label.max": "Maks", + "label.member": "Anggota", + "label.members": "Anggota", "label.min": "Min", "label.mobile": "Ponsel", "label.more": "Lebih banyak", - "label.my-account": "My account", - "label.my-websites": "My websites", + "label.my-account": "Akun saya", + "label.my-websites": "Situs web saya", "label.name": "Nama", "label.new-password": "Kata sandi baru", - "label.none": "None", + "label.none": "Tidak ada", "label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.ok": "OK", "label.os": "OS", - "label.overview": "Overview", + "label.overview": "Tinjauan umum", "label.owner": "Pemilik", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Halaman {current} dari {total}", "label.page-views": "Tampilan halaman", - "label.pageTitle": "Page title", + "label.pageTitle": "Judul halaman", "label.pages": "Halaman", "label.password": "Kata sandi", "label.path": "Path", "label.paths": "Paths", "label.powered-by": "Didukung oleh {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Sebelumnya", + "label.previous-period": "Periode sebelumnya", + "label.previous-year": "Tahun lalu", "label.profile": "Profil", "label.properties": "Properties", "label.property": "Property", @@ -147,133 +147,133 @@ "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Waktu nyata", - "label.referrer": "Referrer", + "label.referrer": "Perujuk", "label.referrers": "Perujuk", "label.refresh": "Segarkan", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.remove-member": "Remove member", - "label.reports": "Reports", + "label.regenerate": "Buat ulang", + "label.region": "Wilayah", + "label.regions": "Wilayah", + "label.remove": "Hapus", + "label.remove-member": "Hapus anggota", + "label.reports": "Laporan", "label.required": "Wajib", "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", - "label.revenue-property": "Revenue Property", + "label.retention": "Retensi", + "label.retention-description": "Ukur daya tarik situs web Anda dengan melacak seberapa sering pengguna kembali.", + "label.revenue": "Pendapatan", + "label.revenue-description": "Lihat pendapatan Anda seiring waktu.", + "label.revenue-property": "Properti pendapatan", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", "label.screens": "Layar", - "label.search": "Search", - "label.select": "Select", - "label.select-date": "Select date", - "label.select-role": "Select role", - "label.select-website": "Select website", - "label.session": "Session", - "label.sessions": "Sessions", + "label.search": "Cari", + "label.select": "Pilih", + "label.select-date": "Pilih tanggal", + "label.select-role": "Pilih role", + "label.select-website": "Pilih situs web", + "label.session": "Sesi", + "label.sessions": "Sesi", "label.settings": "Pengaturan", "label.share-url": "Bagikan URL", "label.single-day": "Sehari", - "label.start-step": "Start Step", - "label.steps": "Steps", + "label.start-step": "Langkah awal", + "label.steps": "Langkah", "label.sum": "Sum", "label.tablet": "Tablet", - "label.team": "Team", - "label.team-id": "Team ID", - "label.team-manager": "Team manager", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", + "label.team": "Tim", + "label.team-id": "ID tim", + "label.team-manager": "Pengelola tim", + "label.team-member": "Anggota tim", + "label.team-name": "Nama tim", + "label.team-owner": "Pemilik tim", "label.team-view-only": "Team view only", - "label.team-websites": "Team websites", - "label.teams": "Teams", + "label.team-websites": "Situs web tim", + "label.teams": "Tim", "label.theme": "Tema", "label.this-month": "Bulan ini", "label.this-week": "Minggu ini", "label.this-year": "Tahun ini", "label.timezone": "Zona waktu", - "label.title": "Title", + "label.title": "Judul", "label.today": "Hari ini", "label.toggle-charts": "Buka grafik", "label.total": "Total", - "label.total-records": "Total records", + "label.total-records": "Total baris", "label.tracking-code": "Kode lacak", - "label.transactions": "Transactions", + "label.transactions": "Transaksi", "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.transfer-website": "Transfer situs web", + "label.true": "Benar", + "label.type": "Tipe", + "label.unique": "Unik", "label.unique-visitors": "Pengunjung unik", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Kustomer unik", "label.unknown": "Tidak diketahui", - "label.untitled": "Untitled", - "label.update": "Update", + "label.untitled": "Tanpa judul", + "label.update": "Perbarui", "label.url": "URL", "label.urls": "URLs", - "label.user": "User", + "label.user": "Pengguna", "label.user-property": "User Property", "label.username": "Nama pengguna", - "label.users": "Users", + "label.users": "Pengguna", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", - "label.value": "Value", - "label.view": "View", + "label.utm-description": "Lacak kampanye Anda melalui parameter UTM.", + "label.value": "Nilai", + "label.view": "Lihat", "label.view-details": "Lihat Detil", - "label.view-only": "View only", + "label.view-only": "Hanya melihat", "label.views": "Tampilan", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "Tampilan per kunjungan", "label.visit-duration": "Waktu kunjungan rata-rata", "label.visitors": "Pengunjung", - "label.visits": "Visits", - "label.website": "Website", - "label.website-id": "Website ID", + "label.visits": "Kunjungan", + "label.website": "Situs web", + "label.website-id": "ID situs web", "label.websites": "Situs web", "label.window": "Window", - "label.yesterday": "Yesterday", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "label.yesterday": "Kemarin", + "message.action-confirmation": "Ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.", "message.active-users": "{x} pengunjung saat ini", - "message.collected-data": "Collected data", + "message.collected-data": "Data dikumpulkan", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-remove": "Are you sure you want to remove {target}?", + "message.confirm-leave": "Apakah Anda yakin ingin meninggalkan {target}?", + "message.confirm-remove": "Apakah Anda yakin ingin menghapus {target}?", "message.confirm-reset": "Anda yakin ingin mengatur ulang statistik {target}?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.delete-team-warning": "Menghapus tim juga akan menghapus semua situs web yang terkait.", "message.delete-website-warning": "Semua data terkait juga akan dihapus.", "message.error": "Ada yang salah.", "message.event-log": "{event} on {url}", "message.go-to-settings": "Pergi ke pengaturan", "message.incorrect-username-password": "Nama pengguna/kata sandi salah.", "message.invalid-domain": "Domain tidak valid", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.min-password-length": "Minimal {n} karakter", + "message.new-version-available": "Versi baru dari Umami {version} telah tersedia!", "message.no-data-available": "Tidak ada data.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Tidak ada data peristiwa", "message.no-match-password": "Kata sandi tidak cocok", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-results-found": "Tidak ada hasil yang ditemukan.", + "message.no-team-websites": "Tim ini tidak memiliki situs web.", + "message.no-teams": "Anda belum membuat tim.", + "message.no-users": "Tidak ada pengguna.", "message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", "message.page-not-found": "Halaman tidak ditemukan.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "Semua statistik pada website ini akan dihapus, tetapi kode lacak akan tetap terpasang", + "message.reset-website": "Untuk mengatur ulang situs web ini, ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.", + "message.reset-website-warning": "Semua statistik pada situs web ini akan dihapus, tetapi kode lacak akan tetap terpasang", "message.saved": "Berhasil disimpan.", "message.share-url": "Ini adalah URL yang dibagikan secara publik untuk {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.team-already-member": "Anda sudah menjadi anggota tim ini.", + "message.team-not-found": "Tim tidak ditemukan.", + "message.team-websites-info": "Situs web dapat dilihat oleh semua anggota tim.", "message.tracking-code": "Kode lacak", - "message.transfer-team-website-to-user": "Transfer this website to your account?", - "message.transfer-user-website-to-team": "Select the team to transfer this website to.", - "message.transfer-website": "Transfer website ownership to your account or another team.", - "message.triggered-event": "Triggered event", - "message.user-deleted": "User deleted.", - "message.viewed-page": "Viewed page", + "message.transfer-team-website-to-user": "Transfer situs web ini ke akun Anda?", + "message.transfer-user-website-to-team": "Pilih tim tujuan untuk mentransfer situs web ini.", + "message.transfer-website": "Transfer kepemilikan situs web ke akun Anda atau tim lain", + "message.triggered-event": "Peristiwa terjadi", + "message.user-deleted": "Pengguna telah dihapus.", + "message.viewed-page": "Halaman dilihat", "message.visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "Pengunjung yang meninggalkan situs web" } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a64210eca..3eddefdcf 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -397,6 +397,14 @@ export const PAID_AD_PARAMS = [ 'epik=', 'ttclid=', 'scid=', + 'aid=', + 'pc_id=', + 'ad_id=', + 'rdt_cid=', + 'ob_click_id=', + 'utm_medium=cpc', + 'utm_medium=paid', + 'utm_medium=paid_social', ]; export const GROUPED_DOMAINS = [ diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index a4ff3a526..d22bad091 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,5 +1,4 @@ import crypto from 'crypto'; -import { startOfHour, startOfMonth } from 'date-fns'; import prand from 'pure-rand'; import { v4, v5 } from 'uuid'; @@ -77,20 +76,8 @@ export function secret() { return hash(process.env.APP_SECRET || process.env.DATABASE_URL); } -export function salt() { - const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString()); - - return hash(secret(), ROTATING_SALT); -} - -export function visitSalt() { - const ROTATING_SALT = hash(startOfHour(new Date()).toUTCString()); - - return hash(secret(), ROTATING_SALT); -} - export function uuid(...args: any) { if (!args.length) return v4(); - return v5(hash(...args, salt()), v5.DNS); + return v5(hash(...args, secret()), v5.DNS); } diff --git a/src/lib/detect.ts b/src/lib/detect.ts index cd91069e4..da2ca8a1b 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -86,13 +86,13 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string = '', headers: Headers) { +export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) { // Ignore local ips if (await isLocalhost(ip)) { return; } - if (!process.env.SKIP_LOCATION_HEADERS) { + if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) { // Cloudflare headers if (headers.get('cf-ipcountry')) { const country = decodeHeader(headers.get('cf-ipcountry')); @@ -147,8 +147,8 @@ export async function getLocation(ip: string = '', headers: Headers) { export async function getClientInfo(request: Request, payload: Record) { const userAgent = payload?.userAgent || request.headers.get('user-agent'); const ip = payload?.ip || getIpAddress(request.headers); - const location = await getLocation(ip, request.headers); - const country = payload?.userAgent || location?.country; + const location = await getLocation(ip, request.headers, !!payload?.ip); + const country = location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; const city = location?.city; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index e2f50a6c7..c8286082e 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -192,7 +192,9 @@ async function parseFilters( options: QueryOptions = {}, ) { const website = await fetchWebsite(websiteId); - const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key)); + const joinSession = Object.keys(filters).find(key => + ['referrer', ...SESSION_COLUMNS].includes(key), + ); return { joinSession: diff --git a/src/lib/request.ts b/src/lib/request.ts index 9d32f89b3..374f1cbc1 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,4 +1,4 @@ -import { ZodObject } from 'zod'; +import { z, ZodSchema } from 'zod'; import { FILTER_COLUMNS } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; @@ -15,7 +15,7 @@ export async function getJsonBody(request: Request) { export async function parseRequest( request: Request, - schema?: ZodObject, + schema?: ZodSchema, options?: { skipAuth: boolean }, ): Promise { const url = new URL(request.url); @@ -24,12 +24,21 @@ export async function parseRequest( let error: () => void | undefined; let auth = null; + const getErrorMessages = (error: z.ZodError) => { + return Object.entries(error.format()) + .map(([key, value]) => { + const messages = (value as any)._errors; + return messages ? `${key}: ${messages.join(', ')}` : null; + }) + .filter(Boolean); + }; + if (schema) { const isGet = request.method === 'GET'; const result = schema.safeParse(isGet ? query : body); if (!result.success) { - error = () => badRequest(result.error); + error = () => badRequest(getErrorMessages(result.error)); } else if (isGet) { query = result.data; } else { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 8df7be9fa..4e2b3e4a3 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -36,6 +36,8 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); +export const anyObjectParam = z.object({}).passthrough(); + export const urlOrPathParam = z.string().refine( value => { try { diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 65ee1175b..148b03f33 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -29,6 +29,7 @@ export async function saveEvent(args: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(args), @@ -49,6 +50,7 @@ async function relationalQuery(data: { eventName?: string; eventData?: any; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -63,6 +65,7 @@ async function relationalQuery(data: { eventData, pageTitle, tag, + createdAt, } = data; const websiteEventId = uuid(); @@ -81,6 +84,7 @@ async function relationalQuery(data: { eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag, + createdAt, }, }); @@ -92,6 +96,7 @@ async function relationalQuery(data: { urlPath: urlPath?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, + createdAt, }); } @@ -121,6 +126,7 @@ async function clickhouseQuery(data: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -139,12 +145,12 @@ async function clickhouseQuery(data: { subdivision2, city, tag, + createdAt, ...args } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; const eventId = uuid(); - const createdAt = getUTCString(); const message = { ...args, @@ -170,7 +176,7 @@ async function clickhouseQuery(data: { event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag: tag, - created_at: createdAt, + created_at: getUTCString(createdAt), }; if (kafka.enabled) { diff --git a/src/queries/sql/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts index 7c158da40..16a5cab10 100644 --- a/src/queries/sql/events/saveEventData.ts +++ b/src/queries/sql/events/saveEventData.ts @@ -15,7 +15,7 @@ export async function saveEventData(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -27,8 +27,9 @@ async function relationalQuery(data: { websiteId: string; eventId: string; eventData: DynamicData; + createdAt?: Date; }): Promise { - const { websiteId, eventId, eventData } = data; + const { websiteId, eventId, eventData, createdAt } = data; const jsonKeys = flattenJSON(eventData); @@ -42,6 +43,7 @@ async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); return prisma.client.eventData.createMany({ @@ -56,7 +58,7 @@ async function clickhouseQuery(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; @@ -77,7 +79,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; }); diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts index 35f0c7126..a060e9a84 100644 --- a/src/queries/sql/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -11,6 +11,7 @@ export async function saveSessionData(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -22,9 +23,10 @@ export async function relationalQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { const { client } = prisma; - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const jsonKeys = flattenJSON(sessionData); @@ -37,6 +39,7 @@ export async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); const existing = await client.sessionData.findMany({ @@ -77,12 +80,12 @@ async function clickhouseQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; - const createdAt = getUTCString(); const jsonKeys = flattenJSON(sessionData); @@ -95,7 +98,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; }); diff --git a/src/tracker/index.js b/src/tracker/index.js index dbd47b7c6..c423a66b5 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -1,11 +1,12 @@ (window => { const { screen: { width, height }, - navigator: { language }, + navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt }, location, document, history, top, + doNotTrack, } = window; const { hostname, href, origin } = location; const { currentScript, referrer } = document; @@ -21,6 +22,7 @@ const hostUrl = attr(_data + 'host-url'); const tag = attr(_data + 'tag'); const autoTrack = attr(_data + 'auto-track') !== _false; + const dnt = attr(_data + 'do-not-track') === _true; const excludeSearch = attr(_data + 'exclude-search') === _true; const excludeHash = attr(_data + 'exclude-hash') === _true; const domain = attr(_data + 'domains') || ''; @@ -46,6 +48,11 @@ tag: tag ? tag : undefined, }); + const hasDoNotTrack = () => { + const dnt = doNotTrack || ndnt || msdnt; + return dnt === 1 || dnt === '1' || dnt === 'yes'; + }; + /* Event handlers */ const handlePush = (state, title, url) => { @@ -182,7 +189,8 @@ disabled || !website || (localStorage && localStorage.getItem('umami.disabled')) || - (domain && !domains.includes(hostname)); + (domain && !domains.includes(hostname)) || + (dnt && hasDoNotTrack()); const send = async (payload, type = 'event') => { if (trackingDisabled()) return;