diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 3bea75f22d718a..bf2bb2acb87cd2 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -10,6 +10,7 @@ import { withMockDate } from './decorators/withMockDate' import { defaultMocks } from '~/mocks/handlers' import { withFeatureFlags } from './decorators/withFeatureFlags' import { withTheme } from './decorators/withTheme' +import { apiHostOrigin } from 'lib/utils/apiHost' const setupMsw = () => { // Make sure the msw worker is started @@ -35,7 +36,7 @@ setupMsw() const setupPosthogJs = () => { // Make sure we don't hit production posthog. We want to control requests to, // e.g. `/decide/` for feature flags - window.JS_POSTHOG_HOST = window.location.origin + window.JS_POSTHOG_HOST = apiHostOrigin() loadPostHogJS() } diff --git a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap index ea4be158d8d053..c7193c7c6dc3e4 100644 --- a/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap +++ b/ee/frontend/mobile-replay/__snapshots__/transform.test.ts.snap @@ -76,6 +76,34 @@ exports[`replay/transform transform can convert images 1`] = ` "tagName": "img", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -146,11 +174,80 @@ exports[`replay/transform transform can convert navigation bar 1`] = ` "childNodes": [ { "attributes": { - "data-rrweb-id": 12345, - "style": "color: #ee3ee4;border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, }, "childNodes": [], - "id": 12345, + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [ + { + "attributes": { + "data-rrweb-id": 12345, + "style": "border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;color: #ee3ee4;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;display:flex;flex-direction:row;align-items:center;justify-content:space-around;color:white;", + }, + "childNodes": [ + { + "attributes": {}, + "childNodes": [ + { + "id": 101, + "textContent": "◀", + "type": 3, + }, + ], + "id": 100, + "tagName": "div", + "type": 2, + }, + { + "attributes": {}, + "childNodes": [ + { + "id": 103, + "textContent": "⚪", + "type": 3, + }, + ], + "id": 102, + "tagName": "div", + "type": 2, + }, + { + "attributes": {}, + "childNodes": [ + { + "id": 105, + "textContent": "⬜️", + "type": 3, + }, + ], + "id": 104, + "tagName": "div", + "type": 2, + }, + ], + "id": 12345, + "tagName": "div", + "type": 2, + }, + ], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, "tagName": "div", "type": 2, }, @@ -225,7 +322,7 @@ exports[`replay/transform transform can convert rect with text 1`] = ` { "attributes": { "data-rrweb-id": 12345, - "style": "color: #ee3ee4;border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;", + "style": "border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;color: #ee3ee4;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;", }, "childNodes": [], "id": 12345, @@ -248,6 +345,34 @@ exports[`replay/transform transform can convert rect with text 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -316,75 +441,68 @@ exports[`replay/transform transform can convert status bar 1`] = ` "style": "height: 100vh; width: 100vw;", }, "childNodes": [ + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, { "attributes": { "data-rrweb-id": 7, - "style": "color: #ee3ee4;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #ee3ee4;border-style: solid;height: 30px;position: fixed;left: 0px;top: 0px;display:flex;flex-direction:row;align-items:center", }, - "childNodes": [ - { - "attributes": { - "data-rrweb-id": 102, - "style": "width: 5px;", - }, - "childNodes": [], - "id": 102, - "tagName": "div", - "type": 2, - }, - { - "attributes": { - "data-rrweb-id": 100, - }, - "childNodes": [ - { - "id": 101, - "textContent": "12:00 AM", - "type": 3, - }, - ], - "id": 100, - "tagName": "div", - "type": 2, - }, - ], + "childNodes": [], "id": 7, "tagName": "div", "type": 2, }, { "attributes": { - "data-rrweb-id": 7, - "style": "width: 100px;height: 0px;position: fixed;left: 13px;top: 17px;display:flex;flex-direction:row;align-items:center", + "data-rrweb-id": 11, }, "childNodes": [ { "attributes": { - "data-rrweb-id": 105, - "style": "width: 5px;", - }, - "childNodes": [], - "id": 105, - "tagName": "div", - "type": 2, - }, - { - "attributes": { - "data-rrweb-id": 103, + "data-rrweb-id": 12, + "style": "color: black;width: 100px;height: 0px;position: fixed;left: 13px;top: 17px;display:flex;flex-direction:row;align-items:center;", }, "childNodes": [ { - "id": 104, - "textContent": "12:00 AM", - "type": 3, + "attributes": { + "data-rrweb-id": 102, + "style": "width: 5px;", + }, + "childNodes": [], + "id": 102, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 100, + }, + "childNodes": [ + { + "id": 101, + "textContent": "12:00 AM", + "type": 3, + }, + ], + "id": 100, + "tagName": "div", + "type": 2, }, ], - "id": 103, + "id": 12, "tagName": "div", "type": 2, }, ], - "id": 7, + "id": 11, "tagName": "div", "type": 2, }, @@ -455,7 +573,36 @@ exports[`replay/transform transform can ignore unknown wireframe types 1`] = ` "data-rrweb-id": 5, "style": "height: 100vh; width: 100vw;", }, - "childNodes": [], + "childNodes": [ + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, + ], "id": 5, "tagName": "body", "type": 2, @@ -538,7 +685,7 @@ exports[`replay/transform transform can process unknown types without error 1`] { "attributes": { "data-rrweb-id": 12345, - "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 25px;top: 42px;align-items: center;justify-content: center;display: flex;", + "style": "background-color: #f3f4ef;color: #35373e;width: 100px;height: 30px;position: fixed;left: 25px;top: 42px;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -551,6 +698,34 @@ exports[`replay/transform transform can process unknown types without error 1`] "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -622,7 +797,7 @@ exports[`replay/transform transform can set background image to base64 png 1`] = { "attributes": { "data-rrweb-id": 12345, - "style": "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=);background-size: auto;background-repeat: no-repeatheight: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", + "style": "background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=');background-size: auto;background-repeat: no-repeat;height: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", }, "childNodes": [], "id": 12345, @@ -632,7 +807,7 @@ exports[`replay/transform transform can set background image to base64 png 1`] = { "attributes": { "data-rrweb-id": 12346, - "style": "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=);background-size: auto;background-repeat: no-repeatheight: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", + "style": "background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=');background-size: auto;background-repeat: no-repeat;height: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", }, "childNodes": [], "id": 12346, @@ -642,7 +817,7 @@ exports[`replay/transform transform can set background image to base64 png 1`] = { "attributes": { "data-rrweb-id": 12346, - "style": "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=);background-size: cover;background-repeat: no-repeatheight: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", + "style": "background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=');background-size: cover;background-repeat: no-repeat;height: 30px;position: fixed;left: 0px;top: 0px;overflow:hidden;white-space:nowrap;", }, "childNodes": [], "id": 12346, @@ -659,6 +834,34 @@ exports[`replay/transform transform can set background image to base64 png 1`] = "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -765,7 +968,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = ` { "attributes": { "data-rrweb-id": 12345, - "style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", + "style": "background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;color: #ffffff;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", }, "childNodes": [ { @@ -781,7 +984,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = ` { "attributes": { "data-rrweb-id": 12345, - "style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", + "style": "background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;color: #ffffff;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", }, "childNodes": [ { @@ -802,7 +1005,7 @@ exports[`replay/transform transform child wireframes are processed 1`] = ` { "attributes": { "data-rrweb-id": 12345, - "style": "color: #ffffff;background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", + "style": "background-color: #000000;border-width: 4px;border-radius: 10px;border-color: #000ddd;border-style: solid;color: #ffffff;width: 100px;height: 30px;position: fixed;left: 11px;top: 12px;overflow:hidden;white-space:nowrap;", }, "childNodes": [ { @@ -820,64 +1023,65 @@ exports[`replay/transform transform child wireframes are processed 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", "type": 2, }, ], - "id": 3, - "tagName": "html", - "type": 2, - }, - ], - "id": 1, - "type": 0, - }, - }, - "timestamp": 1, - "type": 2, - }, -] -`; - -exports[`replay/transform transform incremental mutations de-duplicate the tree 1`] = ` -{ - "data": { - "adds": [ - { - "nextId": null, - "node": { - "attributes": { - "data-rrweb-id": 142908405, - "style": "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAC5CAYAAADNs4/hAAAAAXNSR0IArs4c6QAAAARzQklUCAgI -CHwIZIgAAAWeSURBVHic7dyxqh1lGIbR77cIBuzTWKUShDSChVh7ITaWFoH0Emsr78D7sBVCBFOZ -QMo0XoAEEshvZeeZ8cR9PDywVjsfm7d+mNkzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAADA7Vn/5mjv/fHMfDszX83MgxtdBAAAADDzbGZ+npkf1lqvzo5PA8fe++uZ+XFm -7v73bQAAAADX8npmvllr/XR0dBg49t73Z+b3mblzwWEAAAAA1/FmZj5da7286uCDkx94POIGAAAA -cLvuzMx3RwdngeOzy20BAAAAeG+HjeLKT1T23h/OzJ9zHkEAAAAAbtq7mflorfX6nx6e/QfHvpFJ -AAAAANe01rqyY3g7AwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgT -OAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACA -PIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAA -AMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgA -AACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyB -AwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADI -EzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAA -gDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMA -AADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4 -AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8 -gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAA -yBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAA -AIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIED -AAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgT -OAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACA -PIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAA -AMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgA -AACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIOwscL/6XFQAAAADHDhvFWeD47YJD -AAAAAN7XYaNYRw/33p/PzC/jUxYAAADg9rydmS/WWk+vOjgMF2utJzPz+NKrAAAAAK7h0VHcmDl5 -g+Nve+8vZ+b7mflkZu5dYBgAAADAkT9m5vnMPFxr/XrbYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAgCN/AW0xMqHnNQceAAAAAElFTkSuQmCC -);background-size: auto;background-repeat: no-repeatwidth: 411px;height: 70px;position: fixed;left: 0px;top: 536px;overflow:hidden;white-space:nowrap;", + "id": 3, + "tagName": "html", + "type": 2, + }, + ], + "id": 1, + "type": 0, + }, + }, + "timestamp": 1, + "type": 2, + }, +] +`; + +exports[`replay/transform transform incremental mutations de-duplicate the tree 1`] = ` +{ + "data": { + "adds": [ + { + "nextId": null, + "node": { + "attributes": { + "data-rrweb-id": 142908405, + "style": "background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAC5CAYAAADNs4/hAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAWeSURBVHic7dyxqh1lGIbR77cIBuzTWKUShDSChVh7ITaWFoH0Emsr78D7sBVCBFOZQMo0XoAEEshvZeeZ8cR9PDywVjsfm7d+mNkzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA7Vn/5mjv/fHMfDszX83MgxtdBAAAADDzbGZ+npkf1lqvzo5PA8fe++uZ+XFm7v73bQAAAADX8npmvllr/XR0dBg49t73Z+b3mblzwWEAAAAA1/FmZj5da7286uCDkx94POIGAAAAcLvuzMx3RwdngeOzy20BAAAAeG+HjeLKT1T23h/OzJ9zHkEAAAAAbtq7mflorfX6nx6e/QfHvpFJAAAAANe01rqyY3g7AwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIEzgAAACAPIEDAAAAyBM4AAAAgDyBAwAAAMgTOAAAAIA8gQMAAADIOwscL/6XFQAAAADHDhvFWeD47YJDAAAAAN7XYaNYRw/33p/PzC/jUxYAAADg9rydmS/WWk+vOjgMF2utJzPz+NKrAAAAAK7h0VHcmDl5g+Nve+8vZ+b7mflkZu5dYBgAAADAkT9m5vnMPFxr/XrbYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgCN/AW0xMqHnNQceAAAAAElFTkSuQmCC');background-size: auto;background-repeat: no-repeat;width: 411px;height: 70px;position: fixed;left: 0px;top: 536px;overflow:hidden;white-space:nowrap;", }, "childNodes": [], "id": 142908405, @@ -1347,6 +1551,34 @@ exports[`replay/transform transform inputs buttons with nested elements 1`] = ` "tagName": "button", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1374,7 +1606,7 @@ exports[`replay/transform transform inputs closed keyboard custom event 1`] = ` "attributes": [], "removes": [ { - "id": 6, + "id": 10, "parentId": 5, }, ], @@ -1422,7 +1654,36 @@ exports[`replay/transform transform inputs input - $inputType - hello 1`] = ` "data-rrweb-id": 5, "style": "height: 100vh; width: 100vw;", }, - "childNodes": [], + "childNodes": [ + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, + ], "id": 5, "tagName": "body", "type": 2, @@ -1496,6 +1757,34 @@ exports[`replay/transform transform inputs input - button - click me 1`] = ` "tagName": "button", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1581,6 +1870,34 @@ exports[`replay/transform transform inputs input - checkbox - $value 1`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1665,6 +1982,34 @@ exports[`replay/transform transform inputs input - checkbox - $value 2`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1751,6 +2096,34 @@ exports[`replay/transform transform inputs input - checkbox - $value 3`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1820,6 +2193,34 @@ exports[`replay/transform transform inputs input - checkbox - $value 4`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1889,6 +2290,34 @@ exports[`replay/transform transform inputs input - email - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -1958,6 +2387,34 @@ exports[`replay/transform transform inputs input - number - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2027,6 +2484,34 @@ exports[`replay/transform transform inputs input - password - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2121,6 +2606,34 @@ exports[`replay/transform transform inputs input - progress - $value 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2191,6 +2704,34 @@ exports[`replay/transform transform inputs input - progress - $value 2`] = ` "tagName": "progress", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2261,6 +2802,34 @@ exports[`replay/transform transform inputs input - progress - 0.75 1`] = ` "tagName": "progress", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2331,6 +2900,34 @@ exports[`replay/transform transform inputs input - progress - 0.75 2`] = ` "tagName": "progress", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2400,6 +2997,34 @@ exports[`replay/transform transform inputs input - search - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2501,6 +3126,34 @@ exports[`replay/transform transform inputs input - select - hello 1`] = ` "tagName": "select", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2571,6 +3224,34 @@ exports[`replay/transform transform inputs input - tel - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2640,6 +3321,34 @@ exports[`replay/transform transform inputs input - text - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2709,6 +3418,34 @@ exports[`replay/transform transform inputs input - text - hello 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2778,6 +3515,34 @@ exports[`replay/transform transform inputs input - textArea - $value 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2847,6 +3612,34 @@ exports[`replay/transform transform inputs input - textArea - hello 1`] = ` "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -2918,7 +3711,7 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = ` { "attributes": { "data-rrweb-id": 12357, - "style": "height:100%;flex:1", + "style": "height:100%;flex:1;", }, "childNodes": [ { @@ -2931,7 +3724,7 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = ` "attributes": { "data-rrweb-id": 101, "data-toggle-part": "slider", - "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;background-color:#1d4aff;opacity: 0.2;border-radius:7.5%;", + "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;opacity: 0.2;border-radius:7.5%;background-color:#1d4aff;", }, "childNodes": [], "id": 101, @@ -2942,7 +3735,7 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = ` "attributes": { "data-rrweb-id": 102, "data-toggle-part": "handle", - "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;background-color:#1d4aff;border:2px solid #1d4aff;border-radius:50%;", + "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;border-radius:50%;background-color:#1d4aff;border:2px solid #1d4aff;", }, "childNodes": [], "id": 102, @@ -2964,6 +3757,34 @@ exports[`replay/transform transform inputs input - toggle - $value 1`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -3035,7 +3856,7 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = ` { "attributes": { "data-rrweb-id": 12357, - "style": "height:100%;flex:1", + "style": "height:100%;flex:1;", }, "childNodes": [ { @@ -3048,7 +3869,7 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = ` "attributes": { "data-rrweb-id": 101, "data-toggle-part": "slider", - "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;background-color:#f3f4ef;opacity: 0.2;border-radius:7.5%;", + "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;opacity: 0.2;border-radius:7.5%;background-color:#f3f4ef;", }, "childNodes": [], "id": 101, @@ -3059,7 +3880,7 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = ` "attributes": { "data-rrweb-id": 102, "data-toggle-part": "handle", - "style": "position:absolute;top:1.5%;left:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;background-color:#f3f4ef;border:2px solid #f3f4ef;border-radius:50%;", + "style": "position:absolute;top:1.5%;left:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;border-radius:50%;background-color:#f3f4ef;border:2px solid #f3f4ef;", }, "childNodes": [], "id": 102, @@ -3081,6 +3902,34 @@ exports[`replay/transform transform inputs input - toggle - $value 2`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -3152,7 +4001,7 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = ` { "attributes": { "data-rrweb-id": 12357, - "style": "height:100%;flex:1", + "style": "height:100%;flex:1;", }, "childNodes": [ { @@ -3165,7 +4014,7 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = ` "attributes": { "data-rrweb-id": 101, "data-toggle-part": "slider", - "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;background-color:#1d4aff;opacity: 0.2;border-radius:7.5%;", + "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;opacity: 0.2;border-radius:7.5%;background-color:#1d4aff;", }, "childNodes": [], "id": 101, @@ -3176,7 +4025,7 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = ` "attributes": { "data-rrweb-id": 102, "data-toggle-part": "handle", - "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;background-color:#1d4aff;border:2px solid #1d4aff;border-radius:50%;", + "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;border-radius:50%;background-color:#1d4aff;border:2px solid #1d4aff;", }, "childNodes": [], "id": 102, @@ -3198,6 +4047,34 @@ exports[`replay/transform transform inputs input - toggle - $value 3`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -3271,7 +4148,7 @@ exports[`replay/transform transform inputs input - toggle - $value 4`] = ` "attributes": { "data-rrweb-id": 101, "data-toggle-part": "slider", - "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;background-color:#1d4aff;opacity: 0.2;border-radius:7.5%;", + "style": "position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;opacity: 0.2;border-radius:7.5%;background-color:#1d4aff;", }, "childNodes": [], "id": 101, @@ -3282,7 +4159,7 @@ exports[`replay/transform transform inputs input - toggle - $value 4`] = ` "attributes": { "data-rrweb-id": 102, "data-toggle-part": "handle", - "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;background-color:#1d4aff;border:2px solid #1d4aff;border-radius:50%;", + "style": "position:absolute;top:1.5%;right:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;border-radius:50%;background-color:#1d4aff;border:2px solid #1d4aff;", }, "childNodes": [], "id": 102, @@ -3299,6 +4176,34 @@ exports[`replay/transform transform inputs input - toggle - $value 4`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -3368,6 +4273,34 @@ exports[`replay/transform transform inputs input - url - https://example.io 1`] "tagName": "input", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -3411,7 +4344,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "node": { "attributes": { "data-rrweb-id": 199, - "style": "position: relative; display: flex; flex-direction: row; padding: 2px 4px;", + "style": "position: relative;display: flex;flex-direction: row;padding: 2px 4px;", }, "childNodes": [], "id": 199, @@ -3425,7 +4358,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "node": { "attributes": { "data-rrweb-id": 199, - "style": "position: relative; display: flex; flex-direction: row; padding: 2px 4px;", + "style": "position: relative;display: flex;flex-direction: row;padding: 2px 4px;", }, "childNodes": [], "id": 199, @@ -3440,7 +4373,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 151, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3457,7 +4390,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 151, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3541,7 +4474,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 155, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3558,7 +4491,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 155, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3642,7 +4575,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 159, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3659,7 +4592,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 159, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3743,7 +4676,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 163, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3760,7 +4693,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 163, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3844,7 +4777,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 167, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3861,7 +4794,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 167, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3945,7 +4878,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 171, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -3962,7 +4895,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 171, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4046,7 +4979,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 175, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4063,7 +4996,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 175, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4147,7 +5080,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 179, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4164,7 +5097,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 179, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4248,7 +5181,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 183, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4265,7 +5198,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 183, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4349,7 +5282,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 187, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4366,7 +5299,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 187, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4450,7 +5383,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 191, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4467,7 +5400,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 191, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4551,7 +5484,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 195, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4568,7 +5501,7 @@ exports[`replay/transform transform inputs isolated add mutation 1`] = ` "attributes": { "data-rrweb-id": 195, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4696,7 +5629,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "node": { "attributes": { "data-rrweb-id": 248, - "style": "position: relative; display: flex; flex-direction: row; padding: 2px 4px;", + "style": "position: relative;display: flex;flex-direction: row;padding: 2px 4px;", }, "childNodes": [], "id": 248, @@ -4710,7 +5643,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "node": { "attributes": { "data-rrweb-id": 248, - "style": "position: relative; display: flex; flex-direction: row; padding: 2px 4px;", + "style": "position: relative;display: flex;flex-direction: row;padding: 2px 4px;", }, "childNodes": [], "id": 248, @@ -4725,7 +5658,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 200, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4742,7 +5675,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 200, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4826,7 +5759,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 204, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4843,7 +5776,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 204, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4927,7 +5860,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 208, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -4944,7 +5877,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 208, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5028,7 +5961,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 212, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5045,7 +5978,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 212, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5129,7 +6062,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 216, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5146,7 +6079,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 216, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5230,7 +6163,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 220, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5247,7 +6180,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 220, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5331,7 +6264,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 224, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5348,7 +6281,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 224, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5432,7 +6365,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 228, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5449,7 +6382,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 228, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5533,7 +6466,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 232, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5550,7 +6483,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 232, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5634,7 +6567,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 236, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5651,7 +6584,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 236, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5735,7 +6668,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 240, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5752,7 +6685,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 240, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5836,7 +6769,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 244, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5853,7 +6786,7 @@ exports[`replay/transform transform inputs isolated update mutation 1`] = ` "attributes": { "data-rrweb-id": 244, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [], @@ -5955,8 +6888,8 @@ exports[`replay/transform transform inputs open keyboard custom event 1`] = ` "nextId": null, "node": { "attributes": { - "data-rrweb-id": 6, - "style": "color: #35373e;background-color: #f3f4ef;width: 100vw;height: 150px;bottom: 0;position: fixed;align-items: center;justify-content: center;display: flex;", + "data-rrweb-id": 10, + "style": "background-color: #f3f4ef;color: #35373e;width: 100vw;height: 150px;bottom: 0;position: fixed;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -5965,11 +6898,11 @@ exports[`replay/transform transform inputs open keyboard custom event 1`] = ` "type": 3, }, ], - "id": 6, + "id": 10, "tagName": "div", "type": 2, }, - "parentId": 5, + "parentId": 9, }, { "nextId": null, @@ -5978,7 +6911,7 @@ exports[`replay/transform transform inputs open keyboard custom event 1`] = ` "textContent": "keyboard", "type": 3, }, - "parentId": 6, + "parentId": 10, }, ], "attributes": [], @@ -6031,7 +6964,7 @@ exports[`replay/transform transform inputs placeholder - $inputType - $value 1`] { "attributes": { "data-rrweb-id": 12365, - "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", + "style": "background-color: #f3f4ef;color: #35373e;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -6044,6 +6977,34 @@ exports[`replay/transform transform inputs placeholder - $inputType - $value 1`] "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -6110,14 +7071,14 @@ exports[`replay/transform transform inputs progress rating 1`] = ` { "attributes": { "data-rrweb-id": 148, - "style": "position: relative; display: flex; flex-direction: row; padding: 2px 4px;", + "style": "position: relative;display: flex;flex-direction: row;padding: 2px 4px;", }, "childNodes": [ { "attributes": { "data-rrweb-id": 100, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6158,7 +7119,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 104, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6199,7 +7160,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 108, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6240,7 +7201,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 112, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6281,7 +7242,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 116, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6322,7 +7283,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 120, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6363,7 +7324,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 124, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6404,7 +7365,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 128, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6445,7 +7406,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 132, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6486,7 +7447,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 136, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6527,7 +7488,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 140, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6568,7 +7529,7 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "attributes": { "data-rrweb-id": 144, "fill": "currentColor", - "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden", + "style": "height: 100%;overflow-clip-margin: content-box;overflow:hidden;", "viewBox": "0 0 24 24", }, "childNodes": [ @@ -6615,6 +7576,34 @@ exports[`replay/transform transform inputs progress rating 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -6671,7 +7660,36 @@ exports[`replay/transform transform inputs radio group - $inputType - $value 1`] "data-rrweb-id": 5, "style": "height: 100vh; width: 100vw;", }, - "childNodes": [], + "childNodes": [ + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, + ], "id": 5, "tagName": "body", "type": 2, @@ -6738,6 +7756,34 @@ exports[`replay/transform transform inputs radio_group - $inputType - $value 1`] "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -6805,6 +7851,34 @@ exports[`replay/transform transform inputs radio_group 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -6865,7 +7939,7 @@ exports[`replay/transform transform inputs web_view - $inputType - $value 1`] = { "attributes": { "data-rrweb-id": 12365, - "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", + "style": "background-color: #f3f4ef;color: #35373e;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -6878,6 +7952,34 @@ exports[`replay/transform transform inputs web_view - $inputType - $value 1`] = "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -6938,7 +8040,7 @@ exports[`replay/transform transform inputs web_view with URL 1`] = ` { "attributes": { "data-rrweb-id": 12365, - "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", + "style": "background-color: #f3f4ef;color: #35373e;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -6951,6 +8053,34 @@ exports[`replay/transform transform inputs web_view with URL 1`] = ` "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -7035,6 +8165,34 @@ exports[`replay/transform transform inputs wrapping with labels 1`] = ` "tagName": "label", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", @@ -7096,7 +8254,7 @@ exports[`replay/transform transform omitting x and y is equivalent to setting th { "attributes": { "data-rrweb-id": 12345, - "style": "color: #35373e;background-color: #f3f4ef;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", + "style": "background-color: #f3f4ef;color: #35373e;width: 100px;height: 30px;position: fixed;left: 0px;top: 0px;align-items: center;justify-content: center;display: flex;", }, "childNodes": [ { @@ -7109,6 +8267,34 @@ exports[`replay/transform transform omitting x and y is equivalent to setting th "tagName": "div", "type": 2, }, + { + "attributes": { + "data-render-reason": "a fixed placeholder to contain the keyboard in the correct stacking position", + "data-rrweb-id": 9, + }, + "childNodes": [], + "id": 9, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 7, + }, + "childNodes": [], + "id": 7, + "tagName": "div", + "type": 2, + }, + { + "attributes": { + "data-rrweb-id": 11, + }, + "childNodes": [], + "id": 11, + "tagName": "div", + "type": 2, + }, ], "id": 5, "tagName": "body", diff --git a/ee/frontend/mobile-replay/transform.test.ts b/ee/frontend/mobile-replay/transform.test.ts index 9dd8869a2b981a..9e0ddf6b0e05ee 100644 --- a/ee/frontend/mobile-replay/transform.test.ts +++ b/ee/frontend/mobile-replay/transform.test.ts @@ -5,12 +5,19 @@ import { ifEeDescribe } from 'lib/ee.test' import { PostHogEE } from '../../../frontend/@posthog/ee/types' import * as incrementalSnapshotJson from './__mocks__/increment-with-child-duplication.json' import { validateAgainstWebSchema, validateFromMobile } from './index' +import { wireframe } from './mobile.types' +import { stripBarsFromWireframes } from './transformer/transformers' const unspecifiedBase64ImageURL = 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=' const heartEyesEmojiURL = 'data:image/png;base64,' + unspecifiedBase64ImageURL +function fakeWireframe(type: string, children?: wireframe[]): wireframe { + // this is a fake so we can force the type + return { type, childWireframes: children || [] } as Partial as wireframe +} + describe('replay/transform', () => { describe('validation', () => { test('example of validating incoming _invalid_ data', () => { @@ -1050,4 +1057,40 @@ describe('replay/transform', () => { }) }) }) + + describe('separate status and navbar from other wireframes', () => { + it('no-op', () => { + expect(stripBarsFromWireframes([])).toEqual({ + appNodes: [], + statusBar: undefined, + navigationBar: undefined, + }) + }) + + it('top-level status-bar', () => { + const statusBar = fakeWireframe('status_bar') + expect(stripBarsFromWireframes([statusBar])).toEqual({ appNodes: [], statusBar, navigationBar: undefined }) + }) + + it('top-level nav-bar', () => { + const navBar = fakeWireframe('navigation_bar') + expect(stripBarsFromWireframes([navBar])).toEqual({ + appNodes: [], + statusBar: undefined, + navigationBar: navBar, + }) + }) + + it('nested nav-bar', () => { + const navBar = fakeWireframe('navigation_bar') + const sourceWithNavBar = [ + fakeWireframe('div', [fakeWireframe('div'), fakeWireframe('div', [navBar, fakeWireframe('div')])]), + ] + expect(stripBarsFromWireframes(sourceWithNavBar)).toEqual({ + appNodes: [fakeWireframe('div', [fakeWireframe('div'), fakeWireframe('div', [fakeWireframe('div')])])], + statusBar: undefined, + navigationBar: navBar, + }) + }) + }) }) diff --git a/ee/frontend/mobile-replay/transformer/colors.ts b/ee/frontend/mobile-replay/transformer/colors.ts new file mode 100644 index 00000000000000..56a54b23d723b9 --- /dev/null +++ b/ee/frontend/mobile-replay/transformer/colors.ts @@ -0,0 +1,51 @@ +// from https://gist.github.com/t1grok/a0f6d04db569890bcb57 + +interface rgb { + r: number + g: number + b: number +} +interface yuv { + y: number + u: number + v: number +} + +function hexToRgb(hexColor: string): rgb | null { + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i + hexColor = hexColor.replace(shorthandRegex, function (_, r, g, b) { + return r + r + g + g + b + b + }) + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null +} + +function rgbToYuv(rgbColor: rgb): yuv { + let y, u, v + + y = rgbColor.r * 0.299 + rgbColor.g * 0.587 + rgbColor.b * 0.114 + u = rgbColor.r * -0.168736 + rgbColor.g * -0.331264 + rgbColor.b * 0.5 + 128 + v = rgbColor.r * 0.5 + rgbColor.g * -0.418688 + rgbColor.b * -0.081312 + 128 + + y = Math.floor(y) + u = Math.floor(u) + v = Math.floor(v) + + return { y: y, u: u, v: v } +} + +export const isLight = (hexColor: string): boolean => { + const rgbColor = hexToRgb(hexColor) + if (!rgbColor) { + return false + } + const yuvColor = rgbToYuv(rgbColor) + return yuvColor.y > 128 +} diff --git a/ee/frontend/mobile-replay/transformer/screen-chrome.ts b/ee/frontend/mobile-replay/transformer/screen-chrome.ts new file mode 100644 index 00000000000000..fd64712e9a2247 --- /dev/null +++ b/ee/frontend/mobile-replay/transformer/screen-chrome.ts @@ -0,0 +1,120 @@ +import { NodeType, serializedNodeWithId, wireframeNavigationBar, wireframeStatusBar } from '../mobile.types' +import { isLight } from './colors' +import { NAVIGATION_BAR_ID, STATUS_BAR_ID } from './transformers' +import { ConversionContext, ConversionResult } from './types' +import { asStyleString, makeStylesString } from './wireframeStyle' + +function spacerDiv(idSequence: Generator): serializedNodeWithId { + const spacerId = idSequence.next().value + return { + type: NodeType.Element, + tagName: 'div', + attributes: { + style: 'width: 5px;', + 'data-rrweb-id': spacerId, + }, + id: spacerId, + childNodes: [], + } +} + +function makeFakeNavButton(icon: string, context: ConversionContext): serializedNodeWithId { + return { + type: NodeType.Element, + tagName: 'div', + attributes: {}, + id: context.idSequence.next().value, + childNodes: [ + { + type: NodeType.Text, + textContent: icon, + id: context.idSequence.next().value, + }, + ], + } +} + +export function makeNavigationBar( + wireframe: wireframeNavigationBar, + _children: serializedNodeWithId[], + context: ConversionContext +): ConversionResult | null { + const _id = wireframe.id || NAVIGATION_BAR_ID + + const backArrowTriangle = makeFakeNavButton('◀', context) + const homeCircle = makeFakeNavButton('⚪', context) + const screenButton = makeFakeNavButton('⬜️', context) + + return { + result: { + type: NodeType.Element, + tagName: 'div', + attributes: { + style: asStyleString([ + makeStylesString(wireframe), + 'display:flex', + 'flex-direction:row', + 'align-items:center', + 'justify-content:space-around', + 'color:white', + ]), + 'data-rrweb-id': _id, + }, + id: _id, + childNodes: [backArrowTriangle, homeCircle, screenButton], + }, + context, + } +} + +/** + * tricky: we need to accept children because that's the interface of converters, but we don't use them + */ +export function makeStatusBar( + wireframe: wireframeStatusBar, + _children: serializedNodeWithId[], + context: ConversionContext +): ConversionResult { + const clockId = context.idSequence.next().value + // convert the wireframe timestamp to a date time, then get just the hour and minute of the time from that + const clockTime = context.timestamp + ? new Date(context.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : '' + + const clockFontColor = isLight(wireframe.style?.backgroundColor || '#ffffff') ? 'black' : 'white' + + const clock: serializedNodeWithId = { + type: NodeType.Element, + tagName: 'div', + attributes: { + 'data-rrweb-id': clockId, + }, + id: clockId, + childNodes: [ + { + type: NodeType.Text, + textContent: clockTime, + id: context.idSequence.next().value, + }, + ], + } + + return { + result: { + type: NodeType.Element, + tagName: 'div', + attributes: { + style: asStyleString([ + makeStylesString(wireframe, { color: clockFontColor }), + 'display:flex', + 'flex-direction:row', + 'align-items:center', + ]), + 'data-rrweb-id': STATUS_BAR_ID, + }, + id: STATUS_BAR_ID, + childNodes: [spacerDiv(context.idSequence), clock], + }, + context, + } +} diff --git a/ee/frontend/mobile-replay/transformer/status-bar.ts b/ee/frontend/mobile-replay/transformer/status-bar.ts deleted file mode 100644 index 1ae82c633c4896..00000000000000 --- a/ee/frontend/mobile-replay/transformer/status-bar.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {NodeType, serializedNodeWithId, wireframeStatusBar} from "../mobile.types"; -import {STATUS_BAR_ID} from "./transformers"; -import {ConversionContext, ConversionResult} from "./types"; -import {makeStylesString} from "./wireframeStyle"; - -function spacerDiv(idSequence: Generator): serializedNodeWithId { - const spacerId = idSequence.next().value; - return { - type: NodeType.Element, - tagName: 'div', - attributes: { - 'style': 'width: 5px;', - 'data-rrweb-id': spacerId, - }, - id: spacerId, - childNodes: [], - } -} - -/** - * tricky: we need to accept children because that's the interface of converters, but we don't use them - */ -export function makeStatusBar(wireframe: wireframeStatusBar, _children: serializedNodeWithId[], context: ConversionContext): ConversionResult { - const clockId = context.idSequence.next().value; - // convert the wireframe timestamp to a date time, then get just the hour and minute of the time from that - const clockTime = context.timestamp ? new Date(context.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) : "" - const clock: serializedNodeWithId = { - type: NodeType.Element, - tagName: 'div', - attributes: { - 'data-rrweb-id': clockId, - }, - id: clockId, - childNodes: [ - { - type: NodeType.Text, - textContent: clockTime, - id: context.idSequence.next().value, - }, - ] - }; - - return {result: { - type: NodeType.Element, - tagName: 'div', - attributes: { - style: makeStylesString(wireframe) + 'display:flex;flex-direction:row;align-items:center', - 'data-rrweb-id': STATUS_BAR_ID, - }, - id: STATUS_BAR_ID, - childNodes: [ - spacerDiv(context.idSequence), - clock - ], - }, context } -} diff --git a/ee/frontend/mobile-replay/transformer/transformers.ts b/ee/frontend/mobile-replay/transformer/transformers.ts index 7135bed985ab7c..c717109629f5de 100644 --- a/ee/frontend/mobile-replay/transformer/transformers.ts +++ b/ee/frontend/mobile-replay/transformer/transformers.ts @@ -31,18 +31,21 @@ import { wireframeDiv, wireframeImage, wireframeInputComponent, + wireframeNavigationBar, wireframePlaceholder, wireframeProgress, wireframeRadio, wireframeRadioGroup, wireframeRectangle, wireframeSelect, + wireframeStatusBar, wireframeText, wireframeToggle, } from '../mobile.types' -import { makeStatusBar } from './status-bar' -import { ConversionContext, ConversionResult, StyleOverride } from './types' +import { makeNavigationBar, makeStatusBar } from './screen-chrome' +import { ConversionContext, ConversionResult } from './types' import { + asStyleString, makeBodyStyles, makeColorStyles, makeDeterminateProgressStyles, @@ -85,8 +88,14 @@ const HTML_DOC_TYPE_ID = 2 const HTML_ELEMENT_ID = 3 const HEAD_ID = 4 const BODY_ID = 5 -const KEYBOARD_ID = 6 -export const STATUS_BAR_ID = 7 +// the nav bar should always be the last item in the body so that it is at the top of the stack +const NAVIGATION_BAR_PARENT_ID = 7 +export const NAVIGATION_BAR_ID = 8 +// the keyboard so that it is still before the nav bar +const KEYBOARD_PARENT_ID = 9 +const KEYBOARD_ID = 10 +export const STATUS_BAR_PARENT_ID = 11 +export const STATUS_BAR_ID = 12 function isKeyboardEvent(x: unknown): x is keyboardEvent { return isObject(x) && 'data' in x && isObject(x.data) && 'tag' in x.data && x.data.tag === 'keyboard' @@ -114,7 +123,6 @@ export const makeCustomEvent = ( const shouldAbsolutelyPosition = _isPositiveInteger(mobileCustomEvent.data.payload.x) || _isPositiveInteger(mobileCustomEvent.data.payload.y) - const styleOverride: StyleOverride | undefined = shouldAbsolutelyPosition ? undefined : { bottom: true } const keyboardPlaceHolder = makePlaceholderElement( { id: KEYBOARD_ID, @@ -130,12 +138,14 @@ export const makeCustomEvent = ( timestamp: mobileCustomEvent.timestamp, idSequence: globalIdSequence, skippableNodes: new Set(), - styleOverride, + styleOverride: { + ...(shouldAbsolutelyPosition ? {} : { bottom: true }), + }, } ) if (keyboardPlaceHolder) { adds.push({ - parentId: BODY_ID, + parentId: KEYBOARD_PARENT_ID, nextId: null, node: keyboardPlaceHolder.result, }) @@ -189,7 +199,7 @@ export const makeMetaEvent = ( timestamp: mobileMetaEvent.timestamp, }) -function makeDivElement( +export function makeDivElement( wireframe: wireframeDiv, children: serializedNodeWithId[], context: ConversionContext @@ -200,7 +210,7 @@ function makeDivElement( type: NodeType.Element, tagName: 'div', attributes: { - style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;', + style: asStyleString([makeStylesString(wireframe), 'overflow:hidden', 'white-space:nowrap']), 'data-rrweb-id': _id, }, id: _id, @@ -228,7 +238,7 @@ function makeTextElement( type: NodeType.Element, tagName: 'div', attributes: { - style: makeStylesString(wireframe) + 'overflow:hidden;white-space:nowrap;', + style: asStyleString([makeStylesString(wireframe), 'overflow:hidden', 'white-space:nowrap']), 'data-rrweb-id': wireframe.id, }, id: wireframe.id, @@ -295,6 +305,8 @@ function makePlaceholderElement( } export function dataURIOrPNG(src: string): string { + // replace all new lines in src + src = src.replace(/\r?\n|\r/g, '') if (!src.startsWith('data:image/')) { return 'data:image/png;base64,' + src } @@ -510,7 +522,7 @@ function makeStar(title: string, path: string, context: ConversionContext): seri tagName: 'svg', isSVG: true, attributes: { - style: 'height: 100%;overflow-clip-margin: content-box;overflow:hidden', + style: asStyleString(['height: 100%', 'overflow-clip-margin: content-box', 'overflow:hidden']), viewBox: '0 0 24 24', fill: 'currentColor', 'data-rrweb-id': svgId, @@ -604,9 +616,13 @@ function makeRatingBar( tagName: 'div', id: ratingBarId, attributes: { - style: - makeColorStyles(wireframe) + - 'position: relative; display: flex; flex-direction: row; padding: 2px 4px;', + style: asStyleString([ + makeColorStyles(wireframe), + 'position: relative', + 'display: flex', + 'flex-direction: row', + 'padding: 2px 4px', + ]), 'data-rrweb-id': ratingBarId, }, childNodes: [...filledStars, ...halfStars, ...emptyStars], @@ -725,9 +741,17 @@ function makeToggleParts(wireframe: wireframeToggle, context: ConversionContext) tagName: 'div', attributes: { 'data-toggle-part': 'slider', - style: `position:absolute;top:33%;left:5%;display:inline-block;width:75%;height:33%;background-color:${ - wireframe.style?.color || defaultColor - };opacity: 0.2;border-radius:7.5%;`, + style: asStyleString([ + 'position:absolute', + 'top:33%', + 'left:5%', + 'display:inline-block', + 'width:75%', + 'height:33%', + 'opacity: 0.2', + 'border-radius:7.5%', + `background-color:${wireframe.style?.color || defaultColor}`, + ]), 'data-rrweb-id': sliderPartId, }, id: sliderPartId, @@ -738,11 +762,20 @@ function makeToggleParts(wireframe: wireframeToggle, context: ConversionContext) tagName: 'div', attributes: { 'data-toggle-part': 'handle', - style: `position:absolute;top:1.5%;${togglePosition}:5%;display:flex;align-items:center;justify-content:center;width:40%;height:75%;cursor:inherit;background-color:${ - wireframe.style?.color || defaultColor - };border:2px solid ${ - wireframe.style?.borderColor || wireframe.style?.color || defaultColor - };border-radius:50%;`, + style: asStyleString([ + 'position:absolute', + 'top:1.5%', + `${togglePosition}:5%`, + 'display:flex', + 'align-items:center', + 'justify-content:center', + 'width:40%', + 'height:75%', + 'cursor:inherit', + 'border-radius:50%', + `background-color:${wireframe.style?.color || defaultColor}`, + `border:2px solid ${wireframe.style?.borderColor || wireframe.style?.color || defaultColor}`, + ]), 'data-rrweb-id': handlePartId, }, id: handlePartId, @@ -767,7 +800,7 @@ function makeToggleElement( tagName: 'div', attributes: { // if labelled take up available space, otherwise use provided positioning - style: isLabelled ? `height:100%;flex:1` : makePositionStyles(wireframe), + style: isLabelled ? asStyleString(['height:100%', 'flex:1']) : makePositionStyles(wireframe), 'data-rrweb-id': wireframe.id, }, id: wireframe.id, @@ -777,7 +810,7 @@ function makeToggleElement( tagName: 'div', attributes: { // relative position, fills parent - style: 'position:relative;width:100%;height:100%;', + style: asStyleString(['position:relative', 'width:100%', 'height:100%']), 'data-rrweb-id': wrappingDivId, }, id: wrappingDivId, @@ -911,8 +944,7 @@ function chooseConverter( web_view: makeWebViewElement as any, placeholder: makePlaceholderElement as any, status_bar: makeStatusBar as any, - // we could add in a converter for this, but it's fine without any chrome for now - navigation_bar: makeDivElement as any, + navigation_bar: makeNavigationBar as any, } return converterMapping[converterType] } @@ -1121,6 +1153,109 @@ export const makeIncrementalEvent = ( return converted } +function makeKeyboardParent(): serializedNodeWithId { + return { + type: NodeType.Element, + tagName: 'div', + attributes: { + 'data-render-reason': 'a fixed placeholder to contain the keyboard in the correct stacking position', + 'data-rrweb-id': KEYBOARD_PARENT_ID, + }, + id: KEYBOARD_PARENT_ID, + childNodes: [], + } +} + +function makeStatusBarNode( + statusBar: wireframeStatusBar | undefined, + context: ConversionContext +): serializedNodeWithId { + const childNodes = statusBar ? convertWireframesFor([statusBar], context).result : [] + return { + type: NodeType.Element, + tagName: 'div', + attributes: { + 'data-rrweb-id': STATUS_BAR_PARENT_ID, + }, + id: STATUS_BAR_PARENT_ID, + childNodes, + } +} + +function makeNavBarNode( + navigationBar: wireframeNavigationBar | undefined, + context: ConversionContext +): serializedNodeWithId { + const childNodes = navigationBar ? convertWireframesFor([navigationBar], context).result : [] + return { + type: NodeType.Element, + tagName: 'div', + attributes: { + 'data-rrweb-id': NAVIGATION_BAR_PARENT_ID, + }, + id: NAVIGATION_BAR_PARENT_ID, + childNodes, + } +} + +function stripBarsFromWireframe(wireframe: wireframe): { + wireframe: wireframe | undefined + statusBar: wireframeStatusBar | undefined + navBar: wireframeNavigationBar | undefined +} { + if (wireframe.type === 'status_bar') { + return { wireframe: undefined, statusBar: wireframe, navBar: undefined } + } else if (wireframe.type === 'navigation_bar') { + return { wireframe: undefined, statusBar: undefined, navBar: wireframe } + } else { + let statusBar: wireframeStatusBar | undefined + let navBar: wireframeNavigationBar | undefined + const wireframeToReturn: wireframe | undefined = { ...wireframe } + wireframeToReturn.childWireframes = [] + for (const child of wireframe.childWireframes || []) { + const { + wireframe: childWireframe, + statusBar: childStatusBar, + navBar: childNavBar, + } = stripBarsFromWireframe(child) + statusBar = statusBar || childStatusBar + navBar = navBar || childNavBar + if (childWireframe) { + wireframeToReturn.childWireframes.push(childWireframe) + } + } + return { wireframe: wireframeToReturn, statusBar, navBar } + } +} + +/** + * We want to be able to place the status bar and navigation bar in the correct stacking order. + * So, we lift them out of the tree, and return them separately. + */ +export function stripBarsFromWireframes(wireframes: wireframe[]): { + statusBar: wireframeStatusBar | undefined + navigationBar: wireframeNavigationBar | undefined + appNodes: wireframe[] +} { + let statusBar: wireframeStatusBar | undefined + let navigationBar: wireframeNavigationBar | undefined + const copiedNodes: wireframe[] = [] + + wireframes.forEach((w) => { + const matches = stripBarsFromWireframe(w) + if (matches.statusBar) { + statusBar = matches.statusBar + } + if (matches.navBar) { + navigationBar = matches.navBar + } + if (matches.wireframe) { + copiedNodes.push(matches.wireframe) + } + }) + return { statusBar, navigationBar, appNodes: copiedNodes } +} + export const makeFullEvent = ( mobileEvent: MobileFullSnapshotEvent & { timestamp: number @@ -1140,6 +1275,19 @@ export const makeFullEvent = ( } } + const conversionContext = { + timestamp: mobileEvent.timestamp, + idSequence: globalIdSequence, + } + + const { statusBar, navigationBar, appNodes } = stripBarsFromWireframes(mobileEvent.data.wireframes) + + const nodeGroups = { + appNodes: convertWireframesFor(appNodes, conversionContext).result || [], + statusBarNode: makeStatusBarNode(statusBar, conversionContext), + navBarNode: makeNavBarNode(navigationBar, conversionContext), + } + return { type: EventType.FullSnapshot, timestamp: mobileEvent.timestamp, @@ -1172,11 +1320,14 @@ export const makeFullEvent = ( tagName: 'body', attributes: { style: makeBodyStyles(), 'data-rrweb-id': BODY_ID }, id: BODY_ID, - childNodes: - convertWireframesFor(mobileEvent.data.wireframes, { - timestamp: mobileEvent.timestamp, - idSequence: globalIdSequence, - }).result || [], + childNodes: [ + // in the order they should stack if they ever clash + // lower is higher in the stacking context + ...nodeGroups.appNodes, + makeKeyboardParent(), + nodeGroups.navBarNode, + nodeGroups.statusBarNode, + ], }, ], }, diff --git a/ee/frontend/mobile-replay/transformer/wireframeStyle.ts b/ee/frontend/mobile-replay/transformer/wireframeStyle.ts index 37bd94175a3cbc..fe83800e3c58c4 100644 --- a/ee/frontend/mobile-replay/transformer/wireframeStyle.ts +++ b/ee/frontend/mobile-replay/transformer/wireframeStyle.ts @@ -1,6 +1,26 @@ import { wireframe, wireframeProgress } from '../mobile.types' -import {dataURIOrPNG} from "./transformers"; -import {StyleOverride} from "./types"; +import { dataURIOrPNG } from './transformers' +import { StyleOverride } from './types' + +function ensureTrailingSemicolon(styles: string): string { + return styles.endsWith(';') ? styles : styles + ';' +} + +function stripTrailingSemicolon(styles: string): string { + return styles.endsWith(';') ? styles.slice(0, -1) : styles +} + +export function asStyleString(styleParts: string[]): string { + if (styleParts.length === 0) { + return '' + } + return ensureTrailingSemicolon( + styleParts + .map(stripTrailingSemicolon) + .filter((x) => !!x) + .join(';') + ) +} function isNumber(candidate: unknown): candidate is number { return typeof candidate === 'number' @@ -19,79 +39,79 @@ function ensureUnit(value: string | number): string { } function makeBorderStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' + const styleParts: string[] = [] const combinedStyles = { ...wireframe.style, ...styleOverride, } - if (!combinedStyles) { - return styles - } - if (isUnitLike(combinedStyles.borderWidth)) { const borderWidth = ensureUnit(combinedStyles.borderWidth) - styles += `border-width: ${borderWidth};` + styleParts.push(`border-width: ${borderWidth}`) } if (isUnitLike(combinedStyles.borderRadius)) { const borderRadius = ensureUnit(combinedStyles.borderRadius) - styles += `border-radius: ${borderRadius};` + styleParts.push(`border-radius: ${borderRadius}`) } if (combinedStyles?.borderColor) { - styles += `border-color: ${combinedStyles.borderColor};` + styleParts.push(`border-color: ${combinedStyles.borderColor}`) } - if (styles.length > 0) { - styles += `border-style: solid;` + if (styleParts.length > 0) { + styleParts.push(`border-style: solid`) } - return styles + return asStyleString(styleParts) } export function makeDimensionStyles(wireframe: wireframe): string { - let styles = '' + const styleParts: string[] = [] if (wireframe.width === '100vw') { - styles += `width: 100vw;` + styleParts.push(`width: 100vw`) } else if (isNumber(wireframe.width)) { - styles += `width: ${ensureUnit(wireframe.width)};` + styleParts.push(`width: ${ensureUnit(wireframe.width)}`) } if (isNumber(wireframe.height)) { - styles += `height: ${ensureUnit(wireframe.height)};` + styleParts.push(`height: ${ensureUnit(wireframe.height)}`) } - return styles + return asStyleString(styleParts) } export function makePositionStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' + const styleParts: string[] = [] - styles += makeDimensionStyles(wireframe) + styleParts.push(makeDimensionStyles(wireframe)) if (styleOverride?.bottom) { - styles += `bottom: 0;` - styles += `position: fixed;` + styleParts.push(`bottom: 0`) + styleParts.push(`position: fixed`) } else { const posX = wireframe.x || 0 const posY = wireframe.y || 0 if (isNumber(posX) || isNumber(posY)) { - styles += `position: fixed;` + styleParts.push(`position: fixed`) if (isNumber(posX)) { - styles += `left: ${ensureUnit(posX)};` + styleParts.push(`left: ${ensureUnit(posX)}`) } if (isNumber(posY)) { - styles += `top: ${ensureUnit(posY)};` + styleParts.push(`top: ${ensureUnit(posY)}`) } } } - return styles + if (styleOverride?.['z-index']) { + styleParts.push(`z-index: ${styleOverride['z-index']}`) + } + + return asStyleString(styleParts) } function makeLayoutStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' + const styleParts: string[] = [] const combinedStyles = { ...wireframe.style, @@ -99,91 +119,90 @@ function makeLayoutStyles(wireframe: wireframe, styleOverride?: StyleOverride): } if (combinedStyles.verticalAlign) { - styles += `align-items: ${ - { top: 'flex-start', center: 'center', bottom: 'flex-end' }[combinedStyles.verticalAlign] - };` + styleParts.push( + `align-items: ${{ top: 'flex-start', center: 'center', bottom: 'flex-end' }[combinedStyles.verticalAlign]}` + ) } if (combinedStyles.horizontalAlign) { - styles += `justify-content: ${ - { left: 'flex-start', center: 'center', right: 'flex-end' }[combinedStyles.horizontalAlign] - };` + styleParts.push( + `justify-content: ${ + { left: 'flex-start', center: 'center', right: 'flex-end' }[combinedStyles.horizontalAlign] + }` + ) } - if (styles.length) { - styles += `display: flex;` + if (styleParts.length) { + styleParts.push(`display: flex`) } - return styles + return asStyleString(styleParts) } function makeFontStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' + const styleParts: string[] = [] const combinedStyles = { ...wireframe.style, ...styleOverride, } - if (!combinedStyles) { - return styles - } - if (isUnitLike(combinedStyles.fontSize)) { - styles += `font-size: ${ensureUnit(combinedStyles?.fontSize)};` + styleParts.push(`font-size: ${ensureUnit(combinedStyles?.fontSize)}`) } + if (combinedStyles.fontFamily) { - styles += `font-family: ${combinedStyles.fontFamily};` + styleParts.push(`font-family: ${combinedStyles.fontFamily}`) } - return styles + + return asStyleString(styleParts) } export function makeIndeterminateProgressStyles(wireframe: wireframeProgress, styleOverride?: StyleOverride): string { - let styles = '' const combinedStyles = { ...wireframe.style, ...styleOverride, } - styles += makeBackgroundStyles(wireframe, styleOverride) - styles += makePositionStyles(wireframe) - styles += `border: 4px solid ${combinedStyles.borderColor || combinedStyles.color || 'transparent'};` - styles += `border-radius: 50%;border-top: 4px solid #fff;` - styles += `animation: spin 2s linear infinite;` - return styles + return asStyleString([ + makeBackgroundStyles(wireframe, styleOverride), + makePositionStyles(wireframe), + `border: 4px solid ${combinedStyles.borderColor || combinedStyles.color || 'transparent'};`, + `border-radius: 50%;border-top: 4px solid #fff;`, + `animation: spin 2s linear infinite;`, + ]) } export function makeDeterminateProgressStyles(wireframe: wireframeProgress, styleOverride?: StyleOverride): string { - let styles = '' const combinedStyles = { ...wireframe.style, ...styleOverride, } - styles += makeBackgroundStyles(wireframe, styleOverride) - styles += makePositionStyles(wireframe) - styles += 'border-radius: 50%;' const radialGradient = `radial-gradient(closest-side, white 80%, transparent 0 99.9%, white 0)` const conicGradient = `conic-gradient(${combinedStyles.color || 'black'} calc(${wireframe.value} * 1%), ${ combinedStyles.backgroundColor } 0)` - styles += `background: ${radialGradient}, ${conicGradient};` - return styles + return asStyleString([ + makeBackgroundStyles(wireframe, styleOverride), + makePositionStyles(wireframe), + 'border-radius: 50%', + + `background: ${radialGradient}, ${conicGradient}`, + ]) } /** * normally use makeStylesString instead, but sometimes you need styles without any colors applied * */ export function makeMinimalStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' - - styles += makePositionStyles(wireframe, styleOverride) - styles += makeLayoutStyles(wireframe, styleOverride) - styles += makeFontStyles(wireframe, styleOverride) - - return styles + return asStyleString([ + makePositionStyles(wireframe, styleOverride), + makeLayoutStyles(wireframe, styleOverride), + makeFontStyles(wireframe, styleOverride), + ]) } export function makeBackgroundStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' + let styleParts: string[] = [] const combinedStyles = { ...wireframe.style, @@ -191,51 +210,36 @@ export function makeBackgroundStyles(wireframe: wireframe, styleOverride?: Style } if (combinedStyles.backgroundColor) { - styles += `background-color: ${combinedStyles.backgroundColor};` + styleParts.push(`background-color: ${combinedStyles.backgroundColor}`) } if (combinedStyles.backgroundImage) { - const backgroundImageStyles = [ - `background-image: url(${dataURIOrPNG(combinedStyles.backgroundImage)})`, + styleParts = styleParts.concat([ + `background-image: url('${dataURIOrPNG(combinedStyles.backgroundImage)}')`, `background-size: ${combinedStyles.backgroundSize || 'auto'}`, - 'background-repeat: no-repeat' - ] - - styles += backgroundImageStyles.join(';') + 'background-repeat: no-repeat', + ]) } - return styles + return asStyleString(styleParts) } export function makeColorStyles(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' - const combinedStyles = { ...wireframe.style, ...styleOverride, } + const styleParts = [makeBackgroundStyles(wireframe, styleOverride), makeBorderStyles(wireframe, styleOverride)] if (combinedStyles.color) { - styles += `color: ${combinedStyles.color};` + styleParts.push(`color: ${combinedStyles.color}`) } - styles += makeBackgroundStyles(wireframe, styleOverride) - - styles += makeBorderStyles(wireframe, styleOverride) - - return styles -} -function alwaysEndsWithSemicolon(styles: string): string { - return styles.length > 0 && styles[styles.length - 1] !== ';' ? styles + ';' : styles + return asStyleString(styleParts) } export function makeStylesString(wireframe: wireframe, styleOverride?: StyleOverride): string { - let styles = '' - - styles += makeColorStyles(wireframe, styleOverride) - styles += makeMinimalStyles(wireframe, styleOverride) - - return alwaysEndsWithSemicolon(styles) + return asStyleString([makeColorStyles(wireframe, styleOverride), makeMinimalStyles(wireframe, styleOverride)]) } export function makeHTMLStyles(): string { diff --git a/frontend/__snapshots__/exporter-exporter--dashboard--dark.png b/frontend/__snapshots__/exporter-exporter--dashboard--dark.png index 21929edf1fb4a2..cb9c45f920e8e9 100644 Binary files a/frontend/__snapshots__/exporter-exporter--dashboard--dark.png and b/frontend/__snapshots__/exporter-exporter--dashboard--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--dashboard--light.png b/frontend/__snapshots__/exporter-exporter--dashboard--light.png index 3494fac486a7aa..b789d1fbc8b563 100644 Binary files a/frontend/__snapshots__/exporter-exporter--dashboard--light.png and b/frontend/__snapshots__/exporter-exporter--dashboard--light.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--dark.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--dark.png index a6b4b17cddcb52..8824662e538110 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--dark.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--light.png b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--light.png index 08b55ba654e932..efb0dd869a8f21 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--light.png and b/frontend/__snapshots__/scenes-app-experiments--experiments-list-pay-gate--light.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--dark.png b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--dark.png index 533ff0867e10d1..42103d895b3a76 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--dark.png and b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--light.png b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--light.png index 5d487ebbcf0141..2cf251a9814176 100644 Binary files a/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--light.png and b/frontend/__snapshots__/scenes-app-experiments--view-experiment-pay-gate--light.png differ diff --git a/frontend/public/3000/3000-command-palette-dark.png b/frontend/public/3000/3000-command-palette-dark.png deleted file mode 100644 index b3875f96db422f..00000000000000 Binary files a/frontend/public/3000/3000-command-palette-dark.png and /dev/null differ diff --git a/frontend/public/3000/3000-command-palette.png b/frontend/public/3000/3000-command-palette.png deleted file mode 100644 index 4fab3331e77a88..00000000000000 Binary files a/frontend/public/3000/3000-command-palette.png and /dev/null differ diff --git a/frontend/public/3000/3000-dark-mode.png b/frontend/public/3000/3000-dark-mode.png deleted file mode 100644 index 823f35d5cc30e1..00000000000000 Binary files a/frontend/public/3000/3000-dark-mode.png and /dev/null differ diff --git a/frontend/public/3000/3000-launch.png b/frontend/public/3000/3000-launch.png deleted file mode 100644 index f78f40dc940c95..00000000000000 Binary files a/frontend/public/3000/3000-launch.png and /dev/null differ diff --git a/frontend/public/3000/3000-nav-dark.png b/frontend/public/3000/3000-nav-dark.png deleted file mode 100644 index 0c0994cbf8bd4f..00000000000000 Binary files a/frontend/public/3000/3000-nav-dark.png and /dev/null differ diff --git a/frontend/public/3000/3000-nav.png b/frontend/public/3000/3000-nav.png deleted file mode 100644 index 1f94942fce5ecb..00000000000000 Binary files a/frontend/public/3000/3000-nav.png and /dev/null differ diff --git a/frontend/public/3000/3000-notebooks-dark.png b/frontend/public/3000/3000-notebooks-dark.png deleted file mode 100644 index 9e8e19effcd38e..00000000000000 Binary files a/frontend/public/3000/3000-notebooks-dark.png and /dev/null differ diff --git a/frontend/public/3000/3000-notebooks.png b/frontend/public/3000/3000-notebooks.png deleted file mode 100644 index c806c5eb19cd27..00000000000000 Binary files a/frontend/public/3000/3000-notebooks.png and /dev/null differ diff --git a/frontend/public/3000/3000-search-dark.png b/frontend/public/3000/3000-search-dark.png deleted file mode 100644 index 6a9a770609f9d2..00000000000000 Binary files a/frontend/public/3000/3000-search-dark.png and /dev/null differ diff --git a/frontend/public/3000/3000-search.png b/frontend/public/3000/3000-search.png deleted file mode 100644 index 6ceda306e12992..00000000000000 Binary files a/frontend/public/3000/3000-search.png and /dev/null differ diff --git a/frontend/public/3000/3000-side-panel-dark.png b/frontend/public/3000/3000-side-panel-dark.png deleted file mode 100644 index 35b826c1ae0b87..00000000000000 Binary files a/frontend/public/3000/3000-side-panel-dark.png and /dev/null differ diff --git a/frontend/public/3000/3000-side-panel.png b/frontend/public/3000/3000-side-panel.png deleted file mode 100644 index 4f12f93cf59cf3..00000000000000 Binary files a/frontend/public/3000/3000-side-panel.png and /dev/null differ diff --git a/frontend/public/3000/3000-toolbar.png b/frontend/public/3000/3000-toolbar.png deleted file mode 100644 index f1b5e51f4b6cd1..00000000000000 Binary files a/frontend/public/3000/3000-toolbar.png and /dev/null differ diff --git a/frontend/src/exporter/Exporter.scss b/frontend/src/exporter/Exporter.scss index ff33d1000546f2..df0d456df55d7c 100644 --- a/frontend/src/exporter/Exporter.scss +++ b/frontend/src/exporter/Exporter.scss @@ -17,6 +17,12 @@ body.ExporterBody { max-height: 100vh; } + &--dashboard { + height: 100vh; + max-height: 100vh; + overflow: auto; + } + .SharedDashboard-header { .SharedDashboard-header-team { display: none; diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index 2d20684268abe5..88fb4297aecf24 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -59,6 +59,7 @@ export const navigation3000Logic = kea([ props({} as { inputElement?: HTMLInputElement | null }), connect(() => ({ values: [sceneLogic, ['sceneConfig'], navigationLogic, ['mobileLayout'], teamLogic, ['currentTeam']], + actions: [navigationLogic, ['closeAccountPopover']], })), actions({ hideSidebar: true, @@ -133,6 +134,7 @@ export const navigation3000Logic = kea([ { showNavOnMobile: () => true, hideNavOnMobile: () => false, + closeAccountPopover: () => false, }, ], isSidebarKeyboardShortcutAcknowledged: [ diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx index 43fb8866fee8f3..207e05bbf5cd2f 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.stories.tsx @@ -45,10 +45,6 @@ export const SidePanelDocs: StoryFn = () => { return } -export const SidePanelWelcome: StoryFn = () => { - return -} - export const SidePanelSettings: StoryFn = () => { return } diff --git a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx index b156ec1dba7c61..5b1f7d787db04b 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/SidePanel.tsx @@ -1,6 +1,6 @@ import './SidePanel.scss' -import { IconConfetti, IconEllipsis, IconFeatures, IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons' +import { IconEllipsis, IconFeatures, IconGear, IconInfo, IconNotebook, IconSupport } from '@posthog/icons' import { LemonButton, LemonMenu, LemonMenuItems } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' @@ -19,7 +19,6 @@ import { SidePanelFeaturePreviews } from './panels/SidePanelFeaturePreviews' import { SidePanelSettings } from './panels/SidePanelSettings' import { SidePanelStatus, SidePanelStatusIcon } from './panels/SidePanelStatus' import { SidePanelSupport } from './panels/SidePanelSupport' -import { SidePanelWelcome } from './panels/SidePanelWelcome' import { sidePanelLogic } from './sidePanelLogic' import { sidePanelStateLogic } from './sidePanelStateLogic' @@ -67,11 +66,6 @@ export const SIDE_PANEL_TABS: Record ( - // eslint-disable-next-line react/forbid-dom-props -
- {children} -
-) - -type CardProps = { - children: React.ReactNode - className?: string -} - -const Card = ({ children, className }: CardProps): JSX.Element => ( -
- {children} -
-) - -const Title = ({ children }: { children: React.ReactNode }): JSX.Element => ( -

{children}

-) -const Description = ({ children }: { children: React.ReactNode }): JSX.Element => ( -

{children}

-) -const Image = ({ - src, - alt, - width, - height, - style, -}: { - src: string - alt: string - width?: number | string - height?: number | string - style?: React.CSSProperties - // eslint-disable-next-line react/forbid-dom-props -}): JSX.Element => {alt} - -export const SidePanelWelcome = (): JSX.Element => { - const { closeSidePanel, openSidePanel } = useActions(sidePanelStateLogic) - const { isDarkModeOn } = useValues(themeLogic) - - return ( - <> -
-
- What's new? -
-
- -
-
-
-
-

- 👋 Say hello to -
PostHog 3000
-

-

We're past 0 to 1.

-

- It's time to go from 1 to 3000. And this is just the beginning… -

-
- } - className="mb-5 self-start" - > - Read the blog post - -
-
-
- -
- - - Dark mode - - Toggle between light and dark. Synced with your system by default. - -
- Dark mode -
-
-
- - - - Updated nav - Sub-products are now split out from project & data. -
- Updated nav -
-
- - Notebooks - - Analyze data from different angles and share results with your team - all in - a single document. - -
- Notebooks -
-
-
- - - -
-
- Side panel - - It's this multipurpose thing you're looking at right now! - - Create notebooks, read docs, contact support, and more. -
-
- Side panel -
-
-
-
- - - - Improved search - - Search for anything with - -
- Improved search -
-
- -
- Command bar - - Navigate faster with - -
-
- Command bar -
-
-
- - - - Toolbar redesigned - - Dark mode: on. Same features, but easier to use. And we finally put the "bar" - in "toolbar". - -
- Toolbar -
-
-
- -
- } - > - Read the blog post - - openSidePanel(SidePanelTab.Support, 'feedback:posthog-3000')} - type="secondary" - sideIcon={} - > - Share feedback - -
-
- - - Pro tip: Access this panel again from the{' '} - - - {' '} - menu. - -
-
- - ) -} diff --git a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts index ae109ee3586eaa..1d8a26f983790a 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts +++ b/frontend/src/layout/navigation-3000/sidepanel/panels/sidePanelDocsLogic.ts @@ -25,7 +25,7 @@ export const sidePanelDocsLogic = kea([ path(['scenes', 'navigation', 'sidepanel', 'sidePanelDocsLogic']), connect({ actions: [sidePanelStateLogic, ['openSidePanel', 'closeSidePanel']], - values: [sceneLogic, ['sceneConfig']], + values: [sceneLogic, ['sceneConfig'], sidePanelStateLogic, ['selectedTabOptions']], }), actions({ @@ -85,7 +85,13 @@ export const sidePanelDocsLogic = kea([ })), afterMount(({ actions, values }) => { - if (values.sceneConfig?.defaultDocsPath) { + // If a destination was set in the options, use that + // otherwise the default for the current scene + // otherwise, whatever it last was set to + if (values.selectedTabOptions) { + const initialPath = getPathFromUrl(values.selectedTabOptions) + actions.setInitialPath(initialPath) + } else if (values.sceneConfig?.defaultDocsPath) { actions.setInitialPath(values.sceneConfig?.defaultDocsPath) } }), diff --git a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx index 097930e05eb85c..e889d5783cf0db 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/sidePanelLogic.tsx @@ -1,9 +1,7 @@ -import { afterMount, connect, kea, path, reducers, selectors } from 'kea' -import { subscriptions } from 'kea-subscriptions' +import { connect, kea, path, selectors } from 'kea' import { activationLogic } from 'lib/components/ActivationSidebar/activationLogic' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import posthog from 'posthog-js' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SidePanelTab } from '~/types' @@ -17,7 +15,6 @@ const ALWAYS_EXTRA_TABS = [ SidePanelTab.Settings, SidePanelTab.FeaturePreviews, SidePanelTab.Activity, - SidePanelTab.Welcome, SidePanelTab.Status, ] @@ -42,40 +39,7 @@ export const sidePanelLogic = kea([ actions: [sidePanelStateLogic, ['closeSidePanel', 'openSidePanel']], }), - reducers(() => ({ - welcomeAnnouncementAcknowledged: [ - false, - { persist: true }, - { - closeSidePanel: () => true, - openSidePanel: (_, { tab }) => tab !== SidePanelTab.Welcome, - }, - ], - })), - subscriptions({ - welcomeAnnouncementAcknowledged: (welcomeAnnouncementAcknowledged) => { - if (welcomeAnnouncementAcknowledged) { - // Linked to the FF to ensure it isn't shown again - posthog.capture('3000 welcome acknowledged', { - $set: { - '3000-welcome-acknowledged': true, - }, - }) - } - }, - }), selectors({ - shouldShowWelcomeAnnouncement: [ - (s) => [s.welcomeAnnouncementAcknowledged, s.featureFlags], - (welcomeAnnouncementAcknowledged, featureFlags) => { - if (featureFlags[FEATURE_FLAGS.POSTHOG_3000_WELCOME_ANNOUNCEMENT] && !welcomeAnnouncementAcknowledged) { - return true - } - - return false - }, - ], - enabledTabs: [ (s) => [s.isCloudOrDev, s.isReady, s.hasCompletedAllTasks, s.featureFlags], (isCloudOrDev, isReady, hasCompletedAllTasks, featureflags) => { @@ -95,7 +59,6 @@ export const sidePanelLogic = kea([ } tabs.push(SidePanelTab.FeaturePreviews) tabs.push(SidePanelTab.Settings) - tabs.push(SidePanelTab.Welcome) if (isCloudOrDev && featureflags[FEATURE_FLAGS.SIDEPANEL_STATUS]) { tabs.push(SidePanelTab.Status) @@ -138,10 +101,4 @@ export const sidePanelLogic = kea([ }, ], }), - - afterMount(({ values }) => { - if (!values.sidePanelOpen && values.shouldShowWelcomeAnnouncement) { - sidePanelStateLogic.findMounted()?.actions.openSidePanel(SidePanelTab.Welcome) - } - }), ]) diff --git a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx index 09cddbbea1ddb6..b42cb02e0a6905 100644 --- a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx +++ b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx @@ -1,6 +1,14 @@ import './AccountPopover.scss' -import { IconCheckCircle, IconFeatures, IconGear, IconLive, IconPlusSmall, IconServer } from '@posthog/icons' +import { + IconCheckCircle, + IconConfetti, + IconFeatures, + IconGear, + IconLive, + IconPlusSmall, + IconServer, +} from '@posthog/icons' import { LemonButtonPropsBase } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' @@ -10,7 +18,6 @@ import { Lettermark } from 'lib/lemon-ui/Lettermark' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { billingLogic } from 'scenes/billing/billingLogic' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' import { ThemeSwitcher } from 'scenes/settings/user/ThemeSwitcher' @@ -20,12 +27,13 @@ import { NewOrganizationButton, OtherOrganizationButton, } from '~/layout/navigation/OrganizationSwitcher' +import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' import { organizationLogic } from '../../../scenes/organizationLogic' import { preflightLogic } from '../../../scenes/PreflightCheck/preflightLogic' import { urls } from '../../../scenes/urls' import { userLogic } from '../../../scenes/userLogic' -import { OrganizationBasicType } from '../../../types' +import { OrganizationBasicType, SidePanelTab } from '../../../types' import { navigationLogic } from '../navigationLogic' function AccountPopoverSection({ @@ -182,9 +190,10 @@ function SignOutButton(): JSX.Element { export function AccountPopoverOverlay(): JSX.Element { const { user, otherOrganizations } = useValues(userLogic) const { currentOrganization } = useValues(organizationLogic) - const { preflight } = useValues(preflightLogic) + const { mobileLayout } = useValues(navigationLogic) + const { openSidePanel } = useActions(sidePanelStateLogic) + const { preflight, isCloudOrDev } = useValues(preflightLogic) const { closeAccountPopover } = useActions(navigationLogic) - const { billing } = useValues(billingLogic) return ( <> @@ -193,7 +202,7 @@ export function AccountPopoverOverlay(): JSX.Element { {currentOrganization && } - {preflight?.cloud || !!billing ? ( + {isCloudOrDev ? ( { + closeAccountPopover() + if (!mobileLayout) { + e.preventDefault() + openSidePanel(SidePanelTab.Docs, '/changelog') + } + }} icon={} fullWidth data-attr="whats-new-button" @@ -233,6 +248,19 @@ export function AccountPopoverOverlay(): JSX.Element { {user?.is_staff && } + {!isCloudOrDev && ( + + } + fullWidth + data-attr="top-menu-item-upgrade-to-cloud" + > + Try PostHog Cloud + + + )} diff --git a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts index 652d3571304529..d4412601b22dad 100644 --- a/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts +++ b/frontend/src/lib/components/AuthorizedUrlList/authorizedUrlListLogic.ts @@ -18,6 +18,7 @@ import { encodeParams, urlToAction } from 'kea-router' import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' import { isDomain, isURL } from 'lib/utils' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' @@ -70,7 +71,7 @@ export function appEditorUrl(appUrl: string, actionId?: number | null, defaultIn // the toolbar, which isn't correct when used behind a reverse proxy as // we require e.g. SSO login to the app, which will not work when placed // behind a proxy unless we register each domain with the OAuth2 client. - apiURL: window.location.origin, + apiURL: apiHostOrigin(), appUrl, ...(actionId ? { actionId } : {}), } diff --git a/frontend/src/lib/components/JSSnippet.tsx b/frontend/src/lib/components/JSSnippet.tsx index 3d13f23c49c95d..119d84622b9af1 100644 --- a/frontend/src/lib/components/JSSnippet.tsx +++ b/frontend/src/lib/components/JSSnippet.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' export function JSSnippet(): JSX.Element { @@ -8,9 +9,7 @@ export function JSSnippet(): JSX.Element { return ( {``} ) } diff --git a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx index 1fc4ac3e22fbb3..e8a3330bcdd821 100644 --- a/frontend/src/lib/components/PayGateMini/PayGateMini.tsx +++ b/frontend/src/lib/components/PayGateMini/PayGateMini.tsx @@ -6,6 +6,7 @@ import { useValues } from 'kea' import { FEATURE_MINIMUM_PLAN, POSTHOG_CLOUD_STANDARD_PLAN } from 'lib/constants' import { IconEmojiPeople, IconLightBulb, IconLock, IconPremium } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' +import { capitalizeFirstLetter } from 'lib/utils' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { userLogic } from 'scenes/userLogic' @@ -102,21 +103,22 @@ export function PayGateMini({ children, overrideShouldShowGate, }: PayGateMiniProps): JSX.Element | null { - const { preflight } = useValues(preflightLogic) + const { preflight, isCloudOrDev } = useValues(preflightLogic) const { hasAvailableFeature } = useValues(userLogic) const featureSummary = FEATURE_SUMMARIES[feature] const planRequired = FEATURE_MINIMUM_PLAN[feature] - let gateVariant: 'add-card' | 'contact-sales' | 'subscribe' | null = null + + let gateVariant: 'add-card' | 'contact-sales' | 'move-to-cloud' | null = null if (!overrideShouldShowGate && !hasAvailableFeature(feature)) { - if (preflight?.cloud) { + if (isCloudOrDev) { if (planRequired === POSTHOG_CLOUD_STANDARD_PLAN) { gateVariant = 'add-card' } else { gateVariant = 'contact-sales' } } else { - gateVariant = 'subscribe' + gateVariant = 'move-to-cloud' } } @@ -129,7 +131,11 @@ export function PayGateMini({
{featureSummary.icon || }
{featureSummary.description}
- Subscribe to gain {featureSummary.umbrella}. + {gateVariant === 'move-to-cloud' ? ( + <>{capitalizeFirstLetter(featureSummary.umbrella)} is available on PostHog Cloud. + ) : ( + <>Subscribe to gain {featureSummary.umbrella}. + )} {featureSummary.docsHref && ( <> {' '} @@ -145,8 +151,8 @@ export function PayGateMini({ ? '/organization/billing' : gateVariant === 'contact-sales' ? `mailto:sales@posthog.com?subject=Inquiring about ${featureSummary.umbrella}` - : gateVariant === 'subscribe' - ? '/organization/billing' + : gateVariant === 'move-to-cloud' + ? 'https://us.posthog.com/signup?utm_medium=in-product&utm_campaign=move-to-cloud' : undefined } type="primary" @@ -156,7 +162,7 @@ export function PayGateMini({ ? 'Subscribe now' : gateVariant === 'contact-sales' ? 'Contact sales' - : 'Subscribe'} + : 'Move to PostHog Cloud'}
) : ( diff --git a/frontend/src/lib/components/PayGatePage/PayGatePage.tsx b/frontend/src/lib/components/PayGatePage/PayGatePage.tsx index 383cbe1cdfb43d..d955979048c97d 100644 --- a/frontend/src/lib/components/PayGatePage/PayGatePage.tsx +++ b/frontend/src/lib/components/PayGatePage/PayGatePage.tsx @@ -5,6 +5,8 @@ import { useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { identifierToHuman } from 'lib/utils' import { billingLogic } from 'scenes/billing/billingLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { urls } from 'scenes/urls' import { AvailableFeature } from '~/types' @@ -26,6 +28,7 @@ export function PayGatePage({ featureName, }: PayGatePageInterface): JSX.Element { const { upgradeLink } = useValues(billingLogic) + const { isCloudOrDev } = useValues(preflightLogic) featureName = featureName || identifierToHuman(featureKey, 'title') return ( @@ -33,12 +36,23 @@ export function PayGatePage({

{header}

{caption}
- {!hideUpgradeButton && ( - - Upgrade now to get {featureName} - - )} - {docsLink && ( + {!isCloudOrDev &&

This feature is available on PostHog Cloud.

} + {!hideUpgradeButton && + (isCloudOrDev ? ( + + Upgrade now to get {featureName} + + ) : ( + + Learn more about PostHog Cloud + + ))} + {docsLink && isCloudOrDev && ( = Record>(): ColumnType { - return { - title: normalizeColumnTitle('Created'), - align: 'right', - render: function RenderCreatedAt(_, item): JSX.Element | undefined | '' { - return ( - item.created_at && ( -
- -
- ) - ) - }, - sorter: (a, b) => (new Date(a.created_at) > new Date(b.created_at) ? 1 : -1), - } -} - -export function createdByColumn = Record>(items: T[]): ColumnType { - const { user } = useValues(userLogic) - return { - title: normalizeColumnTitle('Created by'), - render: function Render(_: any, item: any) { - return ( -
- {item.created_by && } - {/* eslint-disable-next-line react/forbid-dom-props */} -
- {item.created_by ? item.created_by.first_name || item.created_by.email : '-'} -
-
- ) - }, - filters: uniqueBy( - items.map((item: T) => { - if (!item.created_by) { - return { - text: '(none)', - value: null, - } - } - return { - text: item.created_by?.first_name || item.created_by?.email, - value: item.created_by?.uuid, - } - }), - (item) => item?.value - ).sort((a, b) => { - // Current user first - if (a.value === user?.uuid) { - return -10 - } - if (b.value === user?.uuid) { - return 10 - } - return (a.text + '').localeCompare(b.text + '') - }), - onFilter: (value, item) => (value === null && item.created_by === null) || item.created_by?.uuid === value, - sorter: (a, b) => - (a.created_by?.first_name || a.created_by?.email || '').localeCompare( - b.created_by?.first_name || b.created_by?.email || '' - ), - } -} diff --git a/frontend/src/lib/components/Table/utils.tsx b/frontend/src/lib/components/Table/utils.tsx deleted file mode 100644 index 8bd296810ae6f3..00000000000000 --- a/frontend/src/lib/components/Table/utils.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useWindowSize } from 'lib/hooks/useWindowSize' -import { getBreakpoint } from 'lib/utils/responsiveUtils' - -export function normalizeColumnTitle(title: string | JSX.Element): JSX.Element { - return {title} -} - -// Returns a boolean indicating whether table should be scrolling or not given a specific -// breakpoint. -interface TableScrollProps { - isTableScrolling: boolean - tableScrollBreakpoint: number - tableScrollX: number | string -} - -export const useIsTableScrolling = (scrollBreakpoint: string): TableScrollProps => { - const { width } = useWindowSize() - const tableScrollBreakpoint = getBreakpoint(scrollBreakpoint) - const isTableScrolling = !!width && width <= tableScrollBreakpoint - - return { - isTableScrolling, - tableScrollBreakpoint, - tableScrollX: isTableScrolling ? 'max-content' : `${tableScrollBreakpoint}px`, - } -} diff --git a/frontend/src/lib/hooks/useBreakpoint.ts b/frontend/src/lib/hooks/useBreakpoint.ts deleted file mode 100644 index cf01e7d80e925e..00000000000000 --- a/frontend/src/lib/hooks/useBreakpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getActiveBreakpointValue } from 'lib/utils/responsiveUtils' -import { useEffect, useState } from 'react' - -import { useWindowSize } from './useWindowSize' - -export const useBreakpoint = (): number => { - const { width } = useWindowSize() - const [breakpoint, setBreakpoint] = useState(getActiveBreakpointValue) - - useEffect(() => { - setBreakpoint(getActiveBreakpointValue) - }, [width]) - - return breakpoint -} diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss index 4a3082f89e9962..5419d9f12b5ab0 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.scss @@ -321,9 +321,7 @@ line-height: 1.5; div { - .posthog-3000 & { - white-space: nowrap; - } + white-space: nowrap; } } diff --git a/frontend/src/lib/utils/apiHost.ts b/frontend/src/lib/utils/apiHost.ts new file mode 100644 index 00000000000000..6b47d8c9c8aead --- /dev/null +++ b/frontend/src/lib/utils/apiHost.ts @@ -0,0 +1,8 @@ +export function apiHostOrigin(): string { + let apiHost = window.location.origin + // similar to https://github.com/PostHog/posthog-js/blob/b79315b7a4fa0caded7026bda2fec01defb0ba73/src/posthog-core.ts#L1742 + if (apiHost === 'https://us.posthog.com') { + apiHost = 'https://app.posthog.com' + } + return apiHost +} diff --git a/frontend/src/lib/utils/responsiveUtils.tsx b/frontend/src/lib/utils/responsiveUtils.tsx deleted file mode 100644 index 0973a25df73ba0..00000000000000 --- a/frontend/src/lib/utils/responsiveUtils.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { responsiveMap } from 'antd/lib/_util/responsiveObserve' - -const BREAKPOINT_MAP = Object.fromEntries( - Object.entries(responsiveMap).map(([key, cssStatement]) => [key, parsePixelValue(cssStatement)]) -) -const BREAKPOINT_VALUES = Object.values(BREAKPOINT_MAP).sort((a, b) => a - b) - -export function parsePixelValue(cssStatement: string): number { - return parseFloat(cssStatement.replace(/[^\d.]/g, '')) -} - -export function getActiveBreakpointValue(): number { - const windowWidth = window.innerWidth - const lastMatchingBreakpoint = BREAKPOINT_VALUES.filter((value) => windowWidth >= value).pop() - return lastMatchingBreakpoint || BREAKPOINT_VALUES[0] -} - -export function getBreakpoint(breakpointKey: string): number { - return BREAKPOINT_MAP[breakpointKey] || -1 -} diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index e9ddb20bc2847a..19f3008fa98a62 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -18,9 +18,11 @@ import { InsightNodeKind, InsightQueryNode, InsightsQueryBase, + LifecycleFilter, NodeKind, PathsFilter, RetentionFilter, + StickinessFilter, TrendsFilter, } from '~/queries/schema' import { @@ -290,21 +292,12 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo // stickiness filter if (isStickinessFilter(filters) && isStickinessQuery(query)) { - query.stickinessFilter = objectCleanWithEmpty({ - display: filters.display, - compare: filters.compare, - showLegend: filters.show_legend, - hidden_legend_indexes: cleanHiddenLegendIndexes(filters.hidden_legend_keys), - showValuesOnSeries: filters.show_values_on_series, - }) + query.stickinessFilter = stickinessFilterToQuery(filters) } // lifecycle filter if (isLifecycleFilter(filters) && isLifecycleQuery(query)) { - query.lifecycleFilter = objectCleanWithEmpty({ - toggledLifecycles: filters.toggledLifecycles, - showValuesOnSeries: filters.show_values_on_series, - }) + query.lifecycleFilter = lifecycleFilterToQuery(filters) } // remove undefined and empty array/objects and return @@ -386,6 +379,23 @@ export const pathsFilterToQuery = (filters: Partial): PathsFilt }) } +export const stickinessFilterToQuery = (filters: Record): StickinessFilter => { + return objectCleanWithEmpty({ + display: filters.display, + compare: filters.compare, + showLegend: filters.show_legend, + hidden_legend_indexes: cleanHiddenLegendIndexes(filters.hidden_legend_keys), + showValuesOnSeries: filters.show_values_on_series, + }) +} + +export const lifecycleFilterToQuery = (filters: Record): LifecycleFilter => { + return objectCleanWithEmpty({ + toggledLifecycles: filters.toggledLifecycles, + showValuesOnSeries: filters.show_values_on_series, + }) +} + export const breakdownFilterToQuery = (filters: Record, isTrends: boolean): BreakdownFilter => { return objectCleanWithEmpty({ breakdown_type: filters.breakdown_type, diff --git a/frontend/src/queries/nodes/InsightQuery/utils/legacy.ts b/frontend/src/queries/nodes/InsightQuery/utils/legacy.ts index 643e5c5de26765..e3b5d73d333d4a 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/legacy.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/legacy.ts @@ -72,3 +72,21 @@ export const isLegacyPathsFilter = (filters: Record | undefined): b ] return legacyKeys.some((key) => key in filters) } + +export const isLegacyStickinessFilter = (filters: Record | undefined): boolean => { + if (filters == null) { + return false + } + + const legacyKeys = ['show_legend', 'hidden_legend_keys', 'show_values_on_series'] + return legacyKeys.some((key) => key in filters) +} + +export const isLegacyLifecycleFilter = (filters: Record | undefined): boolean => { + if (filters == null) { + return false + } + + const legacyKeys = ['show_values_on_series'] + return legacyKeys.some((key) => key in filters) +} diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 22e1ac9fa6855e..9f00d7414fdfb4 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -72,4 +72,5 @@ export const appScenes: Record any> = { [Scene.Onboarding]: () => import('./onboarding/Onboarding'), [Scene.OnboardingProductIntroduction]: () => import('./onboarding/OnboardingProductIntroduction'), [Scene.Settings]: () => import('./settings/SettingsScene'), + [Scene.MoveToPostHogCloud]: () => import('./moveToPostHogCloud/MoveToPostHogCloud'), } diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index b8f693021b551c..23725a938e30a9 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -4,6 +4,7 @@ import { LemonButton, LemonDivider, LemonInput, Link } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Field, Form } from 'kea-forms' +import { router } from 'kea-router' import { SurprisedHog } from 'lib/components/hedgehogs' import { PageHeader } from 'lib/components/PageHeader' import { supportLogic } from 'lib/components/Support/supportLogic' @@ -14,10 +15,10 @@ import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { capitalizeFirstLetter } from 'lib/utils' import { useEffect } from 'react' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' import { BillingHero } from './BillingHero' import { billingLogic } from './billingLogic' @@ -45,10 +46,13 @@ export function Billing(): JSX.Element { isAnnualPlan, } = useValues(billingLogic) const { reportBillingV2Shown } = useActions(billingLogic) - const { preflight } = useValues(preflightLogic) - const cloudOrDev = preflight?.cloud || preflight?.is_debug + const { preflight, isCloudOrDev } = useValues(preflightLogic) const { openSupportForm } = useActions(supportLogic) + if (preflight && !isCloudOrDev) { + router.actions.push(urls.default()) + } + useEffect(() => { if (billing) { reportBillingV2Shown() @@ -155,7 +159,7 @@ export function Billing(): JSX.Element { You are currently on a free trial until {billing.free_trial_until.format('LL')} ) : null} - {!billing?.has_active_subscription && cloudOrDev && ( + {!billing?.has_active_subscription && ( <>
@@ -238,35 +242,6 @@ export function Billing(): JSX.Element {
)} - - {!cloudOrDev && (billing?.license?.plan || !billing?.has_active_subscription) && ( -
- {!cloudOrDev && billing?.license?.plan ? ( -
-
- {capitalizeFirstLetter(billing.license.plan)} license -
- - Please contact sales@posthog.com{' '} - if you would like to make any changes to your license. - -
- ) : null} - - {!cloudOrDev && !billing?.has_active_subscription ? ( -

- Self-hosted licenses are no longer available for purchase. Please contact{' '} - sales@posthog.com to discuss options. -

- ) : null} -
- )} {!isOnboarding && billing?.has_active_subscription && ( diff --git a/frontend/src/scenes/experiments/Experiments.tsx b/frontend/src/scenes/experiments/Experiments.tsx index db5f62c0aa5124..8fb071b9977614 100644 --- a/frontend/src/scenes/experiments/Experiments.tsx +++ b/frontend/src/scenes/experiments/Experiments.tsx @@ -5,7 +5,6 @@ import { ExperimentsHog } from 'lib/components/hedgehogs' import { MemberSelect } from 'lib/components/MemberSelect' import { PageHeader } from 'lib/components/PageHeader' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' -import { normalizeColumnTitle } from 'lib/components/Table/utils' import { dayjs } from 'lib/dayjs' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { More } from 'lib/lemon-ui/LemonButton/More' @@ -59,7 +58,7 @@ export function Experiments(): JSX.Element { const columns: LemonTableColumns = [ { - title: normalizeColumnTitle('Name'), + title: 'Name', dataIndex: 'name', className: 'ph-no-capture', sticky: true, diff --git a/frontend/src/scenes/experiments/ExperimentsPayGate.tsx b/frontend/src/scenes/experiments/ExperimentsPayGate.tsx index 0a0ebe684e6842..7335b3c3b068bc 100644 --- a/frontend/src/scenes/experiments/ExperimentsPayGate.tsx +++ b/frontend/src/scenes/experiments/ExperimentsPayGate.tsx @@ -8,10 +8,10 @@ export function ExperimentsPayGate(): JSX.Element { featureKey={AvailableFeature.EXPERIMENTATION} header={ <> - Introducing Experimentation! + Test changes with statistical significance } - caption="Improve your product by A/B testing new features to discover what works best for your users." + caption="A/B tests, multivariate tests, and robust targeting & exclusion rules. Analyze usage with product analytics and session replay." docsLink="https://posthog.com/docs/user-guides/experimentation" /> ) diff --git a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx index c8877d6ada8beb..c4ad007bb45c5a 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagSnippets.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' import { GroupType } from '~/types' @@ -444,7 +445,7 @@ export function APISnippet({ groupType }: FeatureFlagSnippet): JSX.Element { return ( <> - {`curl ${window.location.origin}/decide?v=3/ \\ + {`curl ${apiHostOrigin()}/decide?v=3/ \\ -X POST -H 'Content-Type: application/json' \\ -d '{ "api_key": "${currentTeam ? currentTeam.api_token : '[project_api_key]'}", diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index 2fd98b4edb2eb8..9021f4e5358d2f 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -8,7 +8,6 @@ import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags' import { PageHeader } from 'lib/components/PageHeader' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' import PropertyFiltersDisplay from 'lib/components/PropertyFilters/components/PropertyFiltersDisplay' -import { normalizeColumnTitle } from 'lib/components/Table/utils' import { FEATURE_FLAGS } from 'lib/constants' import { IconLock } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' @@ -71,7 +70,7 @@ export function OverViewTab({ const columns: LemonTableColumns = [ { - title: normalizeColumnTitle('Key'), + title: 'Key', dataIndex: 'key', className: 'ph-no-capture', sticky: true, diff --git a/frontend/src/scenes/moveToPostHogCloud/MoveToPostHogCloud.tsx b/frontend/src/scenes/moveToPostHogCloud/MoveToPostHogCloud.tsx new file mode 100644 index 00000000000000..c4531f26dee010 --- /dev/null +++ b/frontend/src/scenes/moveToPostHogCloud/MoveToPostHogCloud.tsx @@ -0,0 +1,152 @@ +import { + IconBolt, + IconDatabase, + IconFeatures, + IconFlag, + IconHeart, + IconLock, + IconPrivacy, + IconSupport, + IconTrending, + IconUpload, +} from '@posthog/icons' +import { LemonButton, Link } from '@posthog/lemon-ui' +import { ExperimentsHog } from 'lib/components/hedgehogs' +import { SceneExport } from 'scenes/sceneTypes' + +export const scene: SceneExport = { + component: MoveToPostHogCloud, +} + +type CloudFeature = { + name: string + description: string + icon: JSX.Element + link?: string +} + +const CLOUD_FEATURES: CloudFeature[] = [ + { + name: 'Hosted for you', + description: "No need to worry about servers, databases, or data ingestion. We've got it all covered.", + icon: , + }, + { + name: 'EU and US data centers', + description: 'Host your data in the EU or US, whichever works best for your customer base.', + icon: , + }, + { + name: 'Easy migration', + description: + "We've done this before. It's just a few clicks to get your data moving from self-hosted to Cloud.", + icon: , + link: 'https://posthog.com/docs/migrate/migrate-to-cloud', + }, + { + name: 'Auto-scaling', + description: + 'As your product grows, so does your data. PostHog Cloud scales for you, so you never have to worry about spikes or downtime.', + icon: , + }, + { + name: 'Highly available', + description: 'PostHog Cloud is highly available, so you can rest easy knowing your data is always accessible.', + icon: , + }, + { + name: 'Automatic upgrades', + description: + 'PostHog Cloud is always up to date with the latest features and security updates - no upgrades required.', + icon: , + }, + { + name: 'Automatic backups', + description: "Don't worry about backups - we've got it covered.", + icon: , + }, + { + name: 'Access to all features', + description: + 'Group analytics, data pipelines, A/B testing, and other premium features are only available on PostHog Cloud.', + icon: , + link: 'https://posthog.com/pricing', + }, + { + name: 'World-class support', + description: + 'PostHog Cloud customers get access to our world-class support team, not just the community forum.', + icon: , + link: 'https://posthog.com/handbook/growth/customer-support', + }, + { + name: 'SOC 2 compliant', + description: "We're SOC-2 compliant, so you can rest easy knowing your data is secure.", + icon: , + link: 'https://posthog.com/handbook/company/security', + }, + { + name: 'HIPAA compliant', + description: "Rest easy knowing your customers' data is secure.", + icon: , + }, +] + +export function MoveToPostHogCloud(): JSX.Element { + return ( +
+
+
+
+

PostHog Cloud

+

+ We handle the infra. You focus on your product. +

+

+ Hosting PostHog is no easy feat. It takes a lot of domain nowledge to get it right - + especially at scale. Let us handle the hosting, so you can focus on building your product. +

+
+ + Move to PostHog Cloud + +
+
+ +
+
+
+
+

Features

+
    + {CLOUD_FEATURES.map((feature, i) => { + return ( +
  • + {feature.icon} +

    {feature.name}

    +

    {feature.description}

    + {feature.link && ( +

    + Learn more +

    + )} +
  • + ) + })} +
+
+
+
+ ) +} diff --git a/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts b/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts index f4b890f53ec589..e44cd04bffc7ea 100644 --- a/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts +++ b/frontend/src/scenes/notebooks/Notebook/migrations/migrate.ts @@ -3,14 +3,18 @@ import { JSONContent } from '@tiptap/core' import { breakdownFilterToQuery, funnelsFilterToQuery, + lifecycleFilterToQuery, pathsFilterToQuery, retentionFilterToQuery, + stickinessFilterToQuery, trendsFilterToQuery, } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { isLegacyFunnelsFilter, + isLegacyLifecycleFilter, isLegacyPathsFilter, isLegacyRetentionFilter, + isLegacyStickinessFilter, isLegacyTrendsFilter, } from '~/queries/nodes/InsightQuery/utils/legacy' import { InsightVizNode, NodeKind } from '~/queries/schema' @@ -111,6 +115,14 @@ function convertInsightQueriesToNewSchema(content: JSONContent[]): JSONContent[] query.pathsFilter = pathsFilterToQuery(query.pathsFilter as any) } + if (query.kind === NodeKind.StickinessQuery && isLegacyStickinessFilter(query.stickinessFilter as any)) { + query.stickinessFilter = stickinessFilterToQuery(query.stickinessFilter as any) + } + + if (query.kind === NodeKind.LifecycleQuery && isLegacyLifecycleFilter(query.lifecycleFilter as any)) { + query.lifecycleFilter = lifecycleFilterToQuery(query.lifecycleFilter as any) + } + /* * Breakdown */ diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx index 59f541bf895747..a579fb972b88f4 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx @@ -2,6 +2,7 @@ import { IconCheck, IconMap, IconMessage, IconStack } from '@posthog/icons' import { LemonButton, Link, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { WavingHog } from 'lib/components/hedgehogs' +import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import React from 'react' import { convertLargeNumberToWords } from 'scenes/billing/billing-utils' import { billingProductLogic } from 'scenes/billing/billingProductLogic' @@ -44,6 +45,7 @@ export const Subfeature = ({ name, description, icon_key }: BillingV2FeatureType } const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.Element => { + const { reportOnboardingProductSelected } = useActions(eventUsageLogic) const cta: Partial> = { [ProductKey.SESSION_REPLAY]: 'Start recording my website', [ProductKey.FEATURE_FLAGS]: 'Create a feature flag or experiment', @@ -59,6 +61,9 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E data-attr={`${product.type}-onboarding`} center className="max-w-max" + onClick={() => { + reportOnboardingProductSelected(product.type, false) + }} > {cta[product.type] || 'Get started'} diff --git a/frontend/src/scenes/onboarding/sdks/allSDKs.tsx b/frontend/src/scenes/onboarding/sdks/allSDKs.tsx index 1ef8a6e04dc204..ee375a241ee214 100644 --- a/frontend/src/scenes/onboarding/sdks/allSDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/allSDKs.tsx @@ -11,6 +11,14 @@ export const allSDKs: SDK[] = [ image: require('./logos/javascript_web.svg'), docsLink: 'https://posthog.com/docs/libraries/js', }, + { + name: 'HTML snippet', + key: SDKKey.HTML_SNIPPET, + recommended: true, + tags: [SDKTag.RECOMMENDED, SDKTag.WEB], + image: require('./logos/html.svg'), + docsLink: 'https://posthog.com/docs/libraries/js', + }, { name: 'React', key: SDKKey.REACT, diff --git a/frontend/src/scenes/onboarding/sdks/logos/html.svg b/frontend/src/scenes/onboarding/sdks/logos/html.svg new file mode 100644 index 00000000000000..e9de10d211c87a --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/logos/html.svg @@ -0,0 +1,7 @@ + + HTML5 Logo Badge + + + + + diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx index 181aa04218e40a..e3291c8d381248 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/ProductAnalyticsSDKInstructions.tsx @@ -1,6 +1,7 @@ import { SDKInstructionsMap, SDKKey } from '~/types' import { + HTMLSnippetInstructions, JSWebInstructions, ProductAnalyticsAndroidInstructions, ProductAnalyticsAPIInstructions, @@ -17,6 +18,7 @@ import { export const ProductAnalyticsSDKInstructions: SDKInstructionsMap = { [SDKKey.JS_WEB]: JSWebInstructions, + [SDKKey.HTML_SNIPPET]: HTMLSnippetInstructions, // add next, getsby, and others here [SDKKey.IOS]: ProductAnalyticsIOSInstructions, [SDKKey.REACT_NATIVE]: ProductAnalyticsRNInstructions, diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx index 263d14511865d0..f8409dcc23b1e4 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/api.tsx @@ -1,10 +1,11 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' function APISnippet(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const url = window.location.origin + const url = apiHostOrigin() return ( diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/html-snippet.tsx new file mode 100644 index 00000000000000..48dc2e5a8058f8 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/html-snippet.tsx @@ -0,0 +1,22 @@ +import { LemonDivider } from '@posthog/lemon-ui' +import { CodeSnippet, Language } from 'lib/components/CodeSnippet' + +import { SDKHtmlSnippetInstructions } from '../sdk-install-instructions/html-snippet' + +function JSEventSnippet(): JSX.Element { + return ( + {`posthog.capture('my event', { property: 'value' })`} + ) +} + +export function HTMLSnippetInstructions(): JSX.Element { + return ( + <> + + +

Optional: Send a manual event

+

Our snippet will autocapture events for you, but you can manually define events, too!

+ + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx index dc6b1dd3e0bf8f..72e13e2a4a2950 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/index.tsx @@ -3,6 +3,7 @@ export * from './api' export * from './elixir' export * from './flutter' export * from './go' +export * from './html-snippet' export * from './ios' export * from './js-web' export * from './nodejs' diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx index 5491625dd047e3..fe26ef71b1088e 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/js-web.tsx @@ -14,7 +14,8 @@ export function JSWebInstructions(): JSX.Element { <> -

Send your first event

+

Optional: Send a manual event

+

Our package will autocapture events for you, but you can manually define events, too!

) diff --git a/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx b/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx index 459644b3ec02a6..e7f8ff66acf33b 100644 --- a/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx +++ b/frontend/src/scenes/onboarding/sdks/product-analytics/react-native.tsx @@ -6,7 +6,8 @@ export function ProductAnalyticsRNInstructions(): JSX.Element { return ( <> -

Send an Event

+

Optional: Send a manual event

+

Our package will autocapture events for you, but you can manually define events, too!

{`// With hooks import { usePostHog } from 'posthog-react-native' diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx index 02a004eaada7e4..a663a653275529 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/flutter.tsx @@ -1,14 +1,15 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' function FlutterInstallSnippet(): JSX.Element { - return posthog_flutter: # insert version number + return posthog_flutter: ^3.0.0 } function FlutterAndroidSetupSnippet(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const url = window.location.origin + const url = apiHostOrigin() return ( @@ -23,7 +24,7 @@ function FlutterAndroidSetupSnippet(): JSX.Element { function FlutterIOSSetupSnippet(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const url = window.location.origin + const url = apiHostOrigin() return ( diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/html-snippet.tsx new file mode 100644 index 00000000000000..b816f27be392c0 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/html-snippet.tsx @@ -0,0 +1,14 @@ +import { JSSnippet } from 'lib/components/JSSnippet' + +export function SDKHtmlSnippetInstructions(): JSX.Element { + return ( + <> +

Install

+

+ Just add this snippet to your website within the <head> tag and you'll be ready to + start using PostHog. This can also be used in services like Google Tag Manager. +

+ + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx index e148b79544e722..4b86871452ca04 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ios.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' function IOSInstallCocoaPodsSnippet(): JSX.Element { @@ -27,11 +28,10 @@ import UIKit class AppDelegate: NSObject, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - let POSTHOG_API_KEY = "" - let POSTHOG_HOST = "" + let POSTHOG_API_KEY = "${currentTeam?.api_token}" + let POSTHOG_HOST = "${apiHostOrigin()}" - // TIP: host is optional if you use https://app.posthog.com - let config = PostHogConfig(apiKey: "${currentTeam?.api_token}", host: "${window.location.origin}") + let config = PostHogConfig(apiKey: POSTHOG_API_KEY, host: POSTHOG_HOST) PostHogSDK.shared.setup(config) return true diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx index f560a0f20d3a50..538bf3670ce9ed 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/js-web.tsx @@ -1,7 +1,5 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { JSSnippet } from 'lib/components/JSSnippet' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { teamLogic } from 'scenes/teamLogic' export function JSInstallSnippet(): JSX.Element { @@ -29,17 +27,9 @@ export function JSSetupSnippet(): JSX.Element { export function SDKInstallJSWebInstructions(): JSX.Element { return ( <> -

Option 1. Code snippet

-

- Just add this snippet to your website within the <head> tag and you'll be ready to - start using PostHog.{' '} -

- - -

Option 2. Javascript Library

-

Install the package

+

Install

-

Initialize

+

Initialize

) diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx index 4e00958693031b..2aa0271b2dee53 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/next-js.tsx @@ -1,6 +1,7 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' import { Link } from 'lib/lemon-ui/Link' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' import { JSInstallSnippet } from './js-web' @@ -10,10 +11,9 @@ function NextEnvVarsSnippet(): JSX.Element { return ( - {[ - `NEXT_PUBLIC_POSTHOG_KEY=${currentTeam?.api_token}`, - `NEXT_PUBLIC_POSTHOG_HOST=${window.location.origin}`, - ].join('\n')} + {[`NEXT_PUBLIC_POSTHOG_KEY=${currentTeam?.api_token}`, `NEXT_PUBLIC_POSTHOG_HOST=${apiHostOrigin()}`].join( + '\n' + )} ) } diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx index 336e59a33f17f7..f1204a95f82c26 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/php.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' function PHPConfigSnippet(): JSX.Element { @@ -24,7 +25,7 @@ function PHPSetupSnippet(): JSX.Element { return ( {`PostHog::init('${currentTeam?.api_token}', - array('host' => '${window.location.origin}') + array('host' => '${apiHostOrigin()}') );`} ) diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx index 6b489e0088ad27..b54a7b481d1dd2 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/react-native.tsx @@ -1,11 +1,12 @@ import { Link } from '@posthog/lemon-ui' import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' export function SDKInstallRNInstructions(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const url = window.location.origin + const url = apiHostOrigin() return ( <> diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx index f190fe8b03d046..bc89e9ce5aace0 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/ruby.tsx @@ -1,5 +1,6 @@ import { useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { apiHostOrigin } from 'lib/utils/apiHost' import { teamLogic } from 'scenes/teamLogic' function RubyInstallSnippet(): JSX.Element { @@ -13,7 +14,7 @@ function RubySetupSnippet(): JSX.Element { {`posthog = PostHog::Client.new({ api_key: "${currentTeam?.api_token}", - host: "${window.location.origin}", + host: "${apiHostOrigin()}", on_error: Proc.new { |status, msg| print msg } })`} diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx index bf8566b5dcc776..7e43a06b7faba6 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/SessionReplaySDKInstructions.tsx @@ -1,9 +1,10 @@ import { SDKInstructionsMap, SDKKey } from '~/types' -import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' +import { HTMLSnippetInstructions, JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' export const SessionReplaySDKInstructions: SDKInstructionsMap = { [SDKKey.JS_WEB]: JSWebInstructions, + [SDKKey.HTML_SNIPPET]: HTMLSnippetInstructions, [SDKKey.NEXT_JS]: NextJSInstructions, [SDKKey.REACT]: ReactInstructions, } diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx new file mode 100644 index 00000000000000..ee9c98475dc050 --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/session-replay/html-snippet.tsx @@ -0,0 +1,15 @@ +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + +import { SDKHtmlSnippetInstructions } from '../sdk-install-instructions/html-snippet' +import { SessionReplayFinalSteps } from '../shared-snippets' + +export function HTMLSnippetInstructions(): JSX.Element { + return ( + <> + + +

Final steps

+ + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/session-replay/index.tsx b/frontend/src/scenes/onboarding/sdks/session-replay/index.tsx index 27d9e5388d04d0..bee13a5ce58bb5 100644 --- a/frontend/src/scenes/onboarding/sdks/session-replay/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/session-replay/index.tsx @@ -1,3 +1,4 @@ +export * from './html-snippet' export * from './js-web' export * from './next-js' export * from './react' diff --git a/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx b/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx index ad9db67494c88a..9e2f7f59ae4330 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/SurveysSDKInstructions.tsx @@ -1,9 +1,10 @@ import { SDKInstructionsMap, SDKKey } from '~/types' -import { JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' +import { HTMLSnippetInstructions, JSWebInstructions, NextJSInstructions, ReactInstructions } from '.' export const SurveysSDKInstructions: SDKInstructionsMap = { [SDKKey.JS_WEB]: JSWebInstructions, + [SDKKey.HTML_SNIPPET]: HTMLSnippetInstructions, [SDKKey.NEXT_JS]: NextJSInstructions, [SDKKey.REACT]: ReactInstructions, } diff --git a/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx b/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx new file mode 100644 index 00000000000000..616224d0b2fdcd --- /dev/null +++ b/frontend/src/scenes/onboarding/sdks/surveys/html-snippet.tsx @@ -0,0 +1,14 @@ +import { LemonDivider } from 'lib/lemon-ui/LemonDivider' + +import { SDKHtmlSnippetInstructions } from '../sdk-install-instructions/html-snippet' +import { SurveysFinalSteps } from './SurveysFinalSteps' + +export function HTMLSnippetInstructions(): JSX.Element { + return ( + <> + + + + + ) +} diff --git a/frontend/src/scenes/onboarding/sdks/surveys/index.tsx b/frontend/src/scenes/onboarding/sdks/surveys/index.tsx index 27d9e5388d04d0..bee13a5ce58bb5 100644 --- a/frontend/src/scenes/onboarding/sdks/surveys/index.tsx +++ b/frontend/src/scenes/onboarding/sdks/surveys/index.tsx @@ -1,3 +1,4 @@ +export * from './html-snippet' export * from './js-web' export * from './next-js' export * from './react' diff --git a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx index b0d73e786b74f7..11fb902f5b8e22 100644 --- a/frontend/src/scenes/persons/RelatedFeatureFlags.tsx +++ b/frontend/src/scenes/persons/RelatedFeatureFlags.tsx @@ -1,6 +1,5 @@ import { LemonInput, LemonSelect, LemonTable, LemonTag, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { normalizeColumnTitle } from 'lib/components/Table/utils' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import { LemonTableColumns } from 'lib/lemon-ui/LemonTable' import { capitalizeFirstLetter } from 'lib/utils' @@ -42,7 +41,7 @@ export function RelatedFeatureFlags({ distinctId, groups }: Props): JSX.Element const columns: LemonTableColumns = [ { - title: normalizeColumnTitle('Key'), + title: 'Key', dataIndex: 'key', className: 'ph-no-capture', sticky: true, diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index 2546b06455fe2a..73375a8432cc51 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -199,12 +199,15 @@ export const sceneLogic = kea([ router.actions.replace(urls.login()) return } - if (scene === Scene.Login && preflight?.demo) { // In the demo environment, there's only passwordless "login" via the signup scene router.actions.replace(urls.signup()) return } + if (scene === Scene.MoveToPostHogCloud && preflight?.cloud) { + router.actions.replace(urls.projectHomepage()) + return + } // Redirect to the scene's canonical pathname if needed const currentPathname = router.values.location.pathname @@ -265,17 +268,24 @@ export const sceneLogic = kea([ ) if ( - values.featureFlags[FEATURE_FLAGS.PRODUCT_INTRO_PAGES] === 'test' && productKeyFromUrl && teamLogic.values.currentTeam && !teamLogic.values.currentTeam?.has_completed_onboarding_for?.[productKeyFromUrl] - // TODO: should this only happen when in cloud mode? What is the experience for self-hosted? + // TODO: when removing ff PRODUCT_INTRO_PAGES - should this only happen when in + // cloud mode? What is the experience for self-hosted? ) { - console.warn( - `Onboarding not completed for ${productKeyFromUrl}, redirecting to onboarding intro` - ) - router.actions.replace(urls.onboardingProductIntroduction(productKeyFromUrl)) - return + // TODO: remove after PRODUCT_INTRO_PAGES experiment is complete + posthog.capture('should view onboarding product intro', { + did_view_intro: values.featureFlags[FEATURE_FLAGS.PRODUCT_INTRO_PAGES] === 'test', + product_key: productKeyFromUrl, + }) + if (values.featureFlags[FEATURE_FLAGS.PRODUCT_INTRO_PAGES] === 'test') { + console.warn( + `Onboarding not completed for ${productKeyFromUrl}, redirecting to onboarding intro` + ) + router.actions.replace(urls.onboardingProductIntroduction(productKeyFromUrl)) + return + } } } } diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 8921f6f3072e07..b428fe340961fc 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -77,6 +77,7 @@ export enum Scene { Onboarding = 'Onboarding', OnboardingProductIntroduction = 'OnboardingProductIntroduction', Settings = 'Settings', + MoveToPostHogCloud = 'MoveToPostHogCloud', } export type SceneProps = Record diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 775e94282a8c85..01f81fff3d6ad3 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -376,6 +376,10 @@ export const sceneConfigurations: Record = { projectBased: true, name: 'Settings', }, + [Scene.MoveToPostHogCloud]: { + name: 'Move to PostHog Cloud', + hideProjectNotice: true, + }, } const preserveParams = (url: string) => (_params: Params, searchParams: Params, hashParams: Params) => { @@ -554,4 +558,5 @@ export const routes: Record = { [urls.notebooks()]: Scene.Notebooks, [urls.canvas()]: Scene.Canvas, [urls.settings(':section' as any)]: Scene.Settings, + [urls.moveToPostHogCloud()]: Scene.MoveToPostHogCloud, } diff --git a/frontend/src/scenes/session-recordings/__mocks__/recording_snapshots.ts b/frontend/src/scenes/session-recordings/__mocks__/recording_snapshots.ts index 61a0263ee5680e..11b40c149c9aa4 100644 --- a/frontend/src/scenes/session-recordings/__mocks__/recording_snapshots.ts +++ b/frontend/src/scenes/session-recordings/__mocks__/recording_snapshots.ts @@ -9,6 +9,9 @@ const lineTwo = '{"window_id":"187d7c77dfe1d45-08bdcaf91135a2-1d525634-384000-187d7c77dff39a6","data":[{"type":4,"data":{"href":"http://localhost:3000/","width":2560,"height":1304},"timestamp":1682952388104},{"type":2,"data":{"node":{"type":0,"childNodes":[{"type":1,"name":"html","publicId":"","systemId":"","id":2},{"type":2,"tagName":"html","attributes":{"lang":"en"},"childNodes":[{"type":2,"tagName":"head","attributes":{},"childNodes":[{"type":2,"tagName":"style","attributes":{"data-next-hide-fouc":"true"},"childNodes":[{"type":3,"textContent":"body { display: none; }","isStyle":true,"id":6}],"id":5},{"type":2,"tagName":"noscript","attributes":{"data-next-hide-fouc":"true"},"childNodes":[{"type":3,"textContent":"","id":8}],"id":7},{"type":2,"tagName":"meta","attributes":{"charset":"utf-8"},"childNodes":[],"id":9},{"type":2,"tagName":"title","attributes":{},"childNodes":[{"type":3,"textContent":"PostHog","id":11}],"id":10},{"type":2,"tagName":"meta","attributes":{"name":"viewport","content":"width=device-width, initial-scale=1"},"childNodes":[],"id":12},{"type":2,"tagName":"meta","attributes":{"name":"next-head-count","content":"3"},"childNodes":[],"id":13},{"type":2,"tagName":"noscript","attributes":{"data-n-css":""},"childNodes":[],"id":14},{"type":2,"tagName":"script","attributes":{"defer":"","nomodule":"","src":"http://localhost:3000/_next/static/chunks/polyfills.js?ts=1682952387901"},"childNodes":[],"id":15},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/chunks/webpack.js?ts=1682952387901","defer":""},"childNodes":[],"id":16},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/chunks/main.js?ts=1682952387901","defer":""},"childNodes":[],"id":17},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1682952387901","defer":""},"childNodes":[],"id":18},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/chunks/pages/index.js?ts=1682952387901","defer":""},"childNodes":[],"id":19},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/development/_buildManifest.js?ts=1682952387901","defer":""},"childNodes":[],"id":20},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/development/_ssgManifest.js?ts=1682952387901","defer":""},"childNodes":[],"id":21},{"type":2,"tagName":"style","attributes":{},"childNodes":[{"type":3,"textContent":"main { margin: 0px auto; max-width: 1200px; padding: 2rem; font-family: helvetica, arial, sans-serif; }.buttons { display: flex; gap: 0.5rem; }","isStyle":true,"id":23}],"id":22},{"type":2,"tagName":"noscript","attributes":{"id":"__next_css__DO_NOT_USE__"},"childNodes":[],"id":24}],"id":4},{"type":2,"tagName":"body","attributes":{},"childNodes":[{"type":2,"tagName":"div","attributes":{"id":"__next"},"childNodes":[{"type":2,"tagName":"main","attributes":{},"childNodes":[{"type":2,"tagName":"h1","attributes":{},"childNodes":[{"type":3,"textContent":"PostHog React","id":29}],"id":28},{"type":2,"tagName":"div","attributes":{"class":"buttons"},"childNodes":[{"type":2,"tagName":"button","attributes":{},"childNodes":[{"type":3,"textContent":"Capture event","id":32}],"id":31},{"type":2,"tagName":"button","attributes":{"data-attr":"autocapture-button"},"childNodes":[{"type":3,"textContent":"Autocapture buttons","id":34}],"id":33},{"type":2,"tagName":"button","attributes":{"class":"ph-no-capture","rr_width":"0px","rr_height":"0px"},"childNodes":[],"id":35}],"id":30},{"type":2,"tagName":"p","attributes":{},"childNodes":[{"type":3,"textContent":"Feature flag response: ","id":37}],"id":36}],"id":27}],"id":26},{"type":2,"tagName":"script","attributes":{"type":"text/javascript","src":"http://localhost:8000/static/recorder-v2.js?v=1.53.1"},"childNodes":[],"id":38},{"type":2,"tagName":"script","attributes":{"src":"http://localhost:3000/_next/static/chunks/react-refresh.js?ts=1682952387901"},"childNodes":[],"id":39},{"type":2,"tagName":"script","attributes":{"id":"__NEXT_DATA__","type":"application/json"},"childNodes":[{"type":3,"textContent":"SCRIPT_PLACEHOLDER","id":41}],"id":40}],"id":25}],"id":3}],"id":1},"initialOffset":{"left":0,"top":0}},"timestamp":1682952388106},{"type":3,"data":{"source":0,"texts":[],"attributes":[],"removes":[{"parentId":4,"id":7},{"parentId":4,"id":5}],"adds":[]},"timestamp":1682952388108},{"type":3,"data":{"source":0,"texts":[],"attributes":[],"removes":[],"adds":[{"parentId":25,"nextId":null,"node":{"type":2,"tagName":"div","attributes":{"id":"__next-build-watcher","style":"position: fixed; bottom: 10px; right: 20px; width: 0px; height: 0px; z-index: 99999;"},"childNodes":[],"id":42,"isShadowHost":true}},{"parentId":42,"nextId":null,"node":{"type":2,"tagName":"style","attributes":{},"childNodes":[],"id":43,"isShadow":true}},{"parentId":43,"nextId":null,"node":{"type":3,"textContent":"#container { position: absolute; bottom: 10px; right: 30px; border-radius: 3px; background: rgb(0, 0, 0); color: rgb(255, 255, 255); font: initial; cursor: initial; letter-spacing: initial; text-shadow: initial; text-transform: initial; visibility: initial; padding: 7px 10px 8px; align-items: center; box-shadow: rgba(0, 0, 0, 0.25) 0px 11px 40px 0px, rgba(0, 0, 0, 0.12) 0px 2px 10px 0px; display: none; opacity: 0; transition: opacity 0.1s ease 0s, bottom 0.1s ease 0s; animation: 0.1s ease-in-out 0s 1 normal none running fade-in; }#container.visible { display: flex; }#container.building { bottom: 20px; opacity: 1; }#icon-wrapper { width: 16px; height: 16px; }#icon-wrapper > svg { width: 100%; height: 100%; }#icon-group { animation: 1s ease-in-out 0s infinite normal both running strokedash; }@keyframes fade-in { \\n 0% { bottom: 10px; opacity: 0; }\\n 100% { bottom: 20px; opacity: 1; }\\n}@keyframes strokedash { \\n 0% { stroke-dasharray: 0, 226; }\\n 80%, 100% { stroke-dasharray: 659, 226; }\\n}","isStyle":true,"id":44}},{"parentId":42,"nextId":43,"node":{"type":2,"tagName":"div","attributes":{"id":"container"},"childNodes":[],"id":45,"isShadow":true}},{"parentId":45,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":46}},{"parentId":45,"nextId":46,"node":{"type":2,"tagName":"div","attributes":{"id":"icon-wrapper"},"childNodes":[],"id":47}},{"parentId":45,"nextId":47,"node":{"type":3,"textContent":"\\n ","id":48}},{"parentId":47,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":49}},{"parentId":47,"nextId":49,"node":{"type":2,"tagName":"svg","attributes":{"viewBox":"0 0 226 200"},"childNodes":[],"isSVG":true,"id":50}},{"parentId":47,"nextId":50,"node":{"type":3,"textContent":"\\n ","id":51}},{"parentId":50,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":52}},{"parentId":50,"nextId":52,"node":{"type":2,"tagName":"g","attributes":{"id":"icon-group","fill":"none","stroke":"url(#linear-gradient)","stroke-width":"18"},"childNodes":[],"isSVG":true,"id":53}},{"parentId":50,"nextId":53,"node":{"type":3,"textContent":"\\n ","id":54}},{"parentId":50,"nextId":54,"node":{"type":2,"tagName":"defs","attributes":{},"childNodes":[],"isSVG":true,"id":55}},{"parentId":50,"nextId":55,"node":{"type":3,"textContent":"\\n ","id":56}},{"parentId":55,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":57}},{"parentId":55,"nextId":57,"node":{"type":2,"tagName":"lineargradient","attributes":{"x1":"114.720775%","y1":"181.283245%","x2":"39.5399306%","y2":"100%","id":"linear-gradient"},"childNodes":[],"isSVG":true,"id":58}},{"parentId":55,"nextId":58,"node":{"type":3,"textContent":"\\n ","id":59}},{"parentId":58,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":60}},{"parentId":58,"nextId":60,"node":{"type":2,"tagName":"stop","attributes":{"stop-color":"#FFFFFF","offset":"100%"},"childNodes":[],"isSVG":true,"id":61}},{"parentId":58,"nextId":61,"node":{"type":3,"textContent":"\\n ","id":62}},{"parentId":58,"nextId":62,"node":{"type":2,"tagName":"stop","attributes":{"stop-color":"#000000","offset":"0%"},"childNodes":[],"isSVG":true,"id":63}},{"parentId":58,"nextId":63,"node":{"type":3,"textContent":"\\n ","id":64}},{"parentId":53,"nextId":null,"node":{"type":3,"textContent":"\\n ","id":65}},{"parentId":53,"nextId":65,"node":{"type":2,"tagName":"path","attributes":{"d":"M113,5.08219117 L4.28393801,197.5 L221.716062,197.5 L113,5.08219117 Z"},"childNodes":[],"isSVG":true,"id":66}},{"parentId":53,"nextId":66,"node":{"type":3,"textContent":"\\n ","id":67}}]},"timestamp":1682952388117},{"type":3,"data":{"source":0,"texts":[],"attributes":[],"removes":[{"parentId":10,"id":11},{"parentId":4,"id":12}],"adds":[{"parentId":10,"nextId":null,"node":{"type":3,"textContent":"PostHog","id":68}},{"parentId":4,"nextId":13,"node":{"type":2,"tagName":"meta","attributes":{"name":"viewport","content":"width=device-width, initial-scale=1"},"childNodes":[],"id":69}},{"parentId":25,"nextId":null,"node":{"type":2,"tagName":"next-route-announcer","attributes":{},"childNodes":[],"id":70}},{"parentId":70,"nextId":null,"node":{"type":2,"tagName":"p","attributes":{"aria-live":"assertive","id":"__next-route-announcer__","role":"alert","style":"border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; overflow-wrap: normal;"},"childNodes":[],"id":71}},{"parentId":36,"nextId":null,"node":{"type":3,"textContent":"false","id":72}}]},"timestamp":1682952388132},{"type":3,"data":{"source":1,"positions":[{"x":294,"y":7,"id":26,"timeOffset":0}]},"timestamp":1682952388659},{"type":3,"data":{"source":1,"positions":[{"x":577,"y":269,"id":3,"timeOffset":-438},{"x":684,"y":304,"id":3,"timeOffset":-239},{"x":762,"y":244,"id":3,"timeOffset":-174},{"x":815,"y":203,"id":27,"timeOffset":-123}]},"timestamp":1682952389163},{"type":3,"data":{"source":1,"positions":[{"x":819,"y":197,"id":27,"timeOffset":-427},{"x":831,"y":176,"id":27,"timeOffset":-362},{"x":842,"y":157,"id":36,"timeOffset":-312},{"x":850,"y":142,"id":27,"timeOffset":-261},{"x":852,"y":137,"id":33,"timeOffset":-176},{"x":852,"y":133,"id":33,"timeOffset":-111},{"x":852,"y":133,"id":33,"timeOffset":-28}]},"timestamp":1682952389668},{"type":3,"data":{"source":2,"type":1,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952389698},{"type":3,"data":{"source":2,"type":5,"id":33},"timestamp":1682952389699},{"type":3,"data":{"source":2,"type":0,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952389798},{"type":3,"data":{"source":2,"type":2,"id":33,"x":852,"y":133,"pointerType":0},"timestamp":1682952389798},{"type":3,"data":{"source":2,"type":1,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952389943},{"type":3,"data":{"source":2,"type":0,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952390043},{"type":3,"data":{"source":2,"type":2,"id":33,"x":852,"y":133,"pointerType":0},"timestamp":1682952390044},{"type":3,"data":{"source":2,"type":4,"id":33,"x":852,"y":133},"timestamp":1682952390047},{"type":3,"data":{"source":2,"type":1,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952390112},{"type":3,"data":{"source":2,"type":0,"id":33,"x":852.7421875,"y":133.1640625},"timestamp":1682952390243},{"type":3,"data":{"source":2,"type":2,"id":33,"x":852,"y":133,"pointerType":0},"timestamp":1682952390244},{"type":3,"data":{"source":2,"type":6,"id":33},"timestamp":1682952392745}]}' export const snapshotsAsJSONLines = (): string => `${lineOne}\n${lineTwo}\n` +export const snapshotsAsRealTimeJSONPayload = (): { snapshots: Record[] } => ({ + snapshots: [JSON.parse(lineOne), JSON.parse(lineTwo)], +}) export const convertSnapshotsByWindowId = (snapshotsByWindowId: { [key: string]: eventWithTime[] diff --git a/frontend/src/scenes/session-recordings/player/inspector/__snapshots__/performance-event-utils.test.ts.snap b/frontend/src/scenes/session-recordings/player/inspector/__snapshots__/performance-event-utils.test.ts.snap new file mode 100644 index 00000000000000..6bfcf204548d70 --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/__snapshots__/performance-event-utils.test.ts.snap @@ -0,0 +1,480 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`performance-event-utils can map network events containing server timings 1`] = ` +[ + { + "connect_end": 2.699999988079071, + "connect_start": 2.699999988079071, + "decoded_body_size": 82297, + "dom_complete": 5703.799999982119, + "dom_interactive": 5515.199999988079, + "domain_lookup_end": 2.699999988079071, + "domain_lookup_start": 2.699999988079071, + "duration": 5704.5999999940395, + "encoded_body_size": 82297, + "entry_type": "navigation", + "fetch_start": 2.699999988079071, + "initiator_type": "navigation", + "is_initial": true, + "load_event_end": 5704.5999999940395, + "load_event_start": 5704.5, + "name": "http://localhost:8000/project/1/replay/recent?sessionRecordingId=018d51f0-b624-7b16-964d-f1109f28b3da", + "next_hop_protocol": "http/1.1", + "raw": { + "activationStart": 0, + "connectEnd": 2.699999988079071, + "connectStart": 2.699999988079071, + "criticalCHRestart": 0, + "decodedBodySize": 82297, + "deliveryType": "", + "domComplete": 5703.799999982119, + "domContentLoadedEventEnd": 5515.199999988079, + "domContentLoadedEventStart": 5515.199999988079, + "domInteractive": 5515.199999988079, + "domainLookupEnd": 2.699999988079071, + "domainLookupStart": 2.699999988079071, + "duration": 5704.5999999940395, + "encodedBodySize": 82297, + "endTime": 4892, + "entryType": "navigation", + "fetchStart": 2.699999988079071, + "firstInterimResponseStart": 0, + "initiatorType": "navigation", + "isInitial": true, + "loadEventEnd": 5704.5999999940395, + "loadEventStart": 5704.5, + "name": "http://localhost:8000/project/1/replay/recent?sessionRecordingId=018d51f0-b624-7b16-964d-f1109f28b3da", + "nextHopProtocol": "http/1.1", + "redirectCount": 0, + "redirectEnd": 0, + "redirectStart": 0, + "renderBlockingStatus": "non-blocking", + "requestStart": 9.699999988079071, + "responseEnd": 4891.9000000059605, + "responseStart": 4818.5999999940395, + "responseStatus": 200, + "secureConnectionStart": 0, + "serverTiming": [], + "startTime": 0, + "timeOrigin": 1706482394939, + "timestamp": 1706482394939, + "transferSize": 82597, + "type": "reload", + "unloadEventEnd": 4907.199999988079, + "unloadEventStart": 4907.199999988079, + "workerStart": 0, + }, + "redirect_count": 0, + "redirect_end": 0, + "redirect_start": 0, + "render_blocking_status": "non-blocking", + "request_start": 9.699999988079071, + "response_end": 4891.9000000059605, + "response_start": 4818.5999999940395, + "response_status": 200, + "secure_connection_start": 0, + "start_time": 0, + "time_origin": 1706482394939, + "timestamp": 1706482394939, + "transfer_size": 82597, + "unload_event_end": 4907.199999988079, + "unload_event_start": 4907.199999988079, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + "worker_start": 0, + }, + { + "connect_end": 0, + "connect_start": 0, + "decoded_body_size": 0, + "domain_lookup_end": 0, + "domain_lookup_start": 0, + "duration": 72.09999999403954, + "encoded_body_size": 0, + "entry_type": "resource", + "fetch_start": 5445.199999988079, + "initiator_type": "link", + "is_initial": true, + "name": "http://localhost:8234/static/index.css", + "next_hop_protocol": "", + "raw": { + "connectEnd": 0, + "connectStart": 0, + "decodedBodySize": 0, + "deliveryType": "", + "domainLookupEnd": 0, + "domainLookupStart": 0, + "duration": 72.09999999403954, + "encodedBodySize": 0, + "endTime": 5517, + "entryType": "resource", + "fetchStart": 5445.199999988079, + "firstInterimResponseStart": 0, + "initiatorType": "link", + "isInitial": true, + "name": "http://localhost:8234/static/index.css", + "nextHopProtocol": "", + "redirectEnd": 0, + "redirectStart": 0, + "renderBlockingStatus": "non-blocking", + "requestStart": 0, + "responseEnd": 5517.299999982119, + "responseStart": 0, + "responseStatus": 0, + "secureConnectionStart": 0, + "serverTiming": [], + "startTime": 5445, + "timeOrigin": 1706482394939, + "timestamp": 1706482400384, + "transferSize": 0, + "workerStart": 0, + }, + "redirect_end": 0, + "redirect_start": 0, + "render_blocking_status": "non-blocking", + "request_start": 0, + "response_end": 5517.299999982119, + "response_start": 0, + "response_status": 0, + "secure_connection_start": 0, + "start_time": 5445, + "time_origin": 1706482394939, + "timestamp": 1706482400384, + "transfer_size": 0, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + "worker_start": 0, + }, + { + "connect_end": 10605.799999982119, + "connect_start": 10605.799999982119, + "decoded_body_size": 3011, + "domain_lookup_end": 10605.799999982119, + "domain_lookup_start": 10605.799999982119, + "duration": 2524.300000011921, + "encoded_body_size": 3011, + "entry_type": "resource", + "fetch_start": 10605.799999982119, + "initiator_type": "xmlhttprequest", + "is_initial": true, + "name": "http://localhost:8000/decide/?v=3&ip=1&_=1706482405545&ver=1.103.0", + "next_hop_protocol": "http/1.1", + "raw": { + "connectEnd": 10605.799999982119, + "connectStart": 10605.799999982119, + "decodedBodySize": 3011, + "deliveryType": "", + "domainLookupEnd": 10605.799999982119, + "domainLookupStart": 10605.799999982119, + "duration": 2524.300000011921, + "encodedBodySize": 3011, + "endTime": 13130, + "entryType": "resource", + "fetchStart": 10605.799999982119, + "firstInterimResponseStart": 0, + "initiatorType": "xmlhttprequest", + "isInitial": true, + "name": "http://localhost:8000/decide/?v=3&ip=1&_=1706482405545&ver=1.103.0", + "nextHopProtocol": "http/1.1", + "redirectEnd": 0, + "redirectStart": 0, + "renderBlockingStatus": "non-blocking", + "requestStart": 12689, + "responseEnd": 13130.09999999404, + "responseStart": 13084.40000000596, + "responseStatus": 200, + "secureConnectionStart": 0, + "serverTiming": [], + "startTime": 10606, + "timeOrigin": 1706482394938, + "timestamp": 1706482405543, + "transferSize": 3311, + "workerStart": 0, + }, + "redirect_end": 0, + "redirect_start": 0, + "render_blocking_status": "non-blocking", + "request_start": 12689, + "response_end": 13130.09999999404, + "response_start": 13084.40000000596, + "response_status": 200, + "secure_connection_start": 0, + "start_time": 10606, + "time_origin": 1706482394938, + "timestamp": 1706482405543, + "transfer_size": 3311, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + "worker_start": 0, + }, + { + "connect_end": 12722.59999999404, + "connect_start": 12721.90000000596, + "decoded_body_size": 3337, + "domain_lookup_end": 12721.90000000596, + "domain_lookup_start": 12721.799999982119, + "duration": 2521.800000011921, + "encoded_body_size": 647, + "entry_type": "resource", + "fetch_start": 10640.09999999404, + "initiator_type": "fetch", + "is_initial": true, + "name": "http://localhost:8000/api/projects/1/activity_log/important_changes?unread=true", + "next_hop_protocol": "http/1.1", + "raw": { + "connectEnd": 12722.59999999404, + "connectStart": 12721.90000000596, + "decodedBodySize": 3337, + "deliveryType": "", + "domainLookupEnd": 12721.90000000596, + "domainLookupStart": 12721.799999982119, + "duration": 2521.800000011921, + "encodedBodySize": 647, + "endTime": 13162, + "entryType": "resource", + "fetchStart": 10640.09999999404, + "firstInterimResponseStart": 0, + "initiatorType": "fetch", + "isInitial": true, + "name": "http://localhost:8000/api/projects/1/activity_log/important_changes?unread=true", + "nextHopProtocol": "http/1.1", + "redirectEnd": 0, + "redirectStart": 0, + "renderBlockingStatus": "non-blocking", + "requestStart": 12722.699999988079, + "responseEnd": 13161.90000000596, + "responseStart": 13161.299999982119, + "responseStatus": 200, + "secureConnectionStart": 0, + "serverTiming": [ + {}, + {}, + {}, + {}, + {}, + ], + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + "transferSize": 947, + "workerStart": 0, + }, + "redirect_end": 0, + "redirect_start": 0, + "render_blocking_status": "non-blocking", + "request_start": 12722.699999988079, + "response_end": 13161.90000000596, + "response_start": 13161.299999982119, + "response_status": 200, + "secure_connection_start": 0, + "server_timings": [ + { + "duration": 223.65, + "entry_type": "serverTiming", + "name": "gather_query_parts", + "raw": { + "duration": 223.65, + "entryType": "serverTiming", + "name": "gather_query_parts", + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + }, + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 42.63, + "entry_type": "serverTiming", + "name": "query_for_candidate_ids", + "raw": { + "duration": 42.63, + "entryType": "serverTiming", + "name": "query_for_candidate_ids", + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + }, + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 3.75, + "entry_type": "serverTiming", + "name": "construct_query", + "raw": { + "duration": 3.75, + "entryType": "serverTiming", + "name": "construct_query", + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + }, + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 0.08, + "entry_type": "serverTiming", + "name": "query_for_data", + "raw": { + "duration": 0.08, + "entryType": "serverTiming", + "name": "query_for_data", + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + }, + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 39.98, + "entry_type": "serverTiming", + "name": "serialize", + "raw": { + "duration": 39.98, + "entryType": "serverTiming", + "name": "serialize", + "startTime": 10640, + "timeOrigin": 1706482394938, + "timestamp": 1706482405578, + }, + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + ], + "start_time": 10640, + "time_origin": 1706482394938, + "timestamp": 1706482405578, + "transfer_size": 947, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + "worker_start": 0, + }, + { + "connect_end": 10654.299999982119, + "connect_start": 10654.299999982119, + "decoded_body_size": 61551, + "domain_lookup_end": 10654.299999982119, + "domain_lookup_start": 10654.299999982119, + "duration": 2927.100000023842, + "encoded_body_size": 2789, + "entry_type": "resource", + "fetch_start": 10654.299999982119, + "initiator_type": "fetch", + "is_initial": true, + "name": "http://localhost:8000/api/projects/1/session_recordings?session_recording_duration=%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A1%2C%22operator%22%3A%22gt%22%7D&properties=%5B%5D&events=%5B%5D&actions=%5B%5D&date_from=-7d&console_logs=%5B%5D&person_uuid=&limit=20", + "next_hop_protocol": "http/1.1", + "raw": { + "connectEnd": 10654.299999982119, + "connectStart": 10654.299999982119, + "decodedBodySize": 61551, + "deliveryType": "", + "domainLookupEnd": 10654.299999982119, + "domainLookupStart": 10654.299999982119, + "duration": 2927.100000023842, + "encodedBodySize": 2789, + "endTime": 13581, + "entryType": "resource", + "fetchStart": 10654.299999982119, + "firstInterimResponseStart": 0, + "initiatorType": "fetch", + "isInitial": true, + "name": "http://localhost:8000/api/projects/1/session_recordings?session_recording_duration=%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A1%2C%22operator%22%3A%22gt%22%7D&properties=%5B%5D&events=%5B%5D&actions=%5B%5D&date_from=-7d&console_logs=%5B%5D&person_uuid=&limit=20", + "nextHopProtocol": "http/1.1", + "redirectEnd": 0, + "redirectStart": 0, + "renderBlockingStatus": "non-blocking", + "requestStart": 12803.699999988079, + "responseEnd": 13581.40000000596, + "responseStart": 13577, + "responseStatus": 200, + "secureConnectionStart": 0, + "serverTiming": [ + {}, + {}, + {}, + ], + "startTime": 10654, + "timeOrigin": 1706482394938, + "timestamp": 1706482405592, + "transferSize": 3089, + "workerStart": 0, + }, + "redirect_end": 0, + "redirect_start": 0, + "render_blocking_status": "non-blocking", + "request_start": 12803.699999988079, + "response_end": 13581.40000000596, + "response_start": 13577, + "response_status": 200, + "secure_connection_start": 0, + "server_timings": [ + { + "duration": 384.21, + "entry_type": "serverTiming", + "name": "load_recordings_from_clickhouse", + "raw": { + "duration": 384.21, + "entryType": "serverTiming", + "name": "load_recordings_from_clickhouse", + "startTime": 10654, + "timeOrigin": 1706482394938, + "timestamp": 1706482405592, + }, + "start_time": 10654, + "time_origin": 1706482394938, + "timestamp": 1706482405592, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 0.72, + "entry_type": "serverTiming", + "name": "load_persons", + "raw": { + "duration": 0.72, + "entryType": "serverTiming", + "name": "load_persons", + "startTime": 10654, + "timeOrigin": 1706482394938, + "timestamp": 1706482405592, + }, + "start_time": 10654, + "time_origin": 1706482394938, + "timestamp": 1706482405592, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + { + "duration": 7.12, + "entry_type": "serverTiming", + "name": "process_persons", + "raw": { + "duration": 7.12, + "entryType": "serverTiming", + "name": "process_persons", + "startTime": 10654, + "timeOrigin": 1706482394938, + "timestamp": 1706482405592, + }, + "start_time": 10654, + "time_origin": 1706482394938, + "timestamp": 1706482405592, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + }, + ], + "start_time": 10654, + "time_origin": 1706482394938, + "timestamp": 1706482405592, + "transfer_size": 3089, + "window_id": "018d5247-079c-7126-8e43-464605576a62", + "worker_start": 0, + }, +] +`; diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx index 32869955855815..10c4cb545efe92 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -206,9 +206,15 @@ export function ItemPerformanceEvent({ } if ( - ['response_headers', 'request_headers', 'request_body', 'response_body', 'response_status', 'raw'].includes( - key - ) + [ + 'response_headers', + 'request_headers', + 'request_body', + 'response_body', + 'response_status', + 'raw', + 'server_timings', + ].includes(key) ) { return acc } diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx index 797bab213024d7..3059668772a6df 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx @@ -9,6 +9,7 @@ import { SimpleKeyValueList } from 'scenes/session-recordings/player/inspector/c import { PerformanceEvent } from '~/types' export interface EventPerformanceMeasure { + label?: string start: number end: number color: string @@ -25,6 +26,7 @@ const perfSections = [ 'waiting for first byte', 'receiving response', 'document processing', + 'server_timing', ] as const const perfDescriptions: Record<(typeof perfSections)[number], string> = { @@ -40,6 +42,8 @@ const perfDescriptions: Record<(typeof perfSections)[number], string> = { 'receiving response': 'The time taken to receive the response from the server.', 'document processing': 'The time taken to process the document after the response from the server has been received.', + server_timing: + 'Servers can optionally report backend timings, these are a name and duration. The spec does not include a start time, so we use the start time of the request associated with the timing.', } function colorForSection(section: (typeof perfSections)[number]): string { @@ -62,12 +66,18 @@ function colorForSection(section: (typeof perfSections)[number]): string { return getSeriesColor(9) case 'document processing': return getSeriesColor(10) - default: + case 'server_timing': return getSeriesColor(11) + default: + return getSeriesColor(12) } } -type PerformanceMeasures = Record +// most sections are single events, but server timings can be multiple +type PerformanceMeasures = { + networkTimings: Record + serverTimings: EventPerformanceMeasure[] +} /** * There are defined sections to performance measurement. We may have data for some or all of them @@ -109,10 +119,15 @@ type PerformanceMeasures = Record * - from response_end * - until load_event_end * + * 8) also server timings + * - these have only duration, no start time + * - and are sometimes reported from backends to the browser + * * see https://nicj.net/resourcetiming-in-practice/ */ export function calculatePerformanceParts(perfEntry: PerformanceEvent): PerformanceMeasures { const performanceParts: Record = {} + const serverTimings: EventPerformanceMeasure[] = [] if (isPresent(perfEntry.redirect_start) && isPresent(perfEntry.redirect_end)) { if (perfEntry.redirect_end - perfEntry.redirect_start > 0) { @@ -212,7 +227,20 @@ export function calculatePerformanceParts(perfEntry: PerformanceEvent): Performa } } - return performanceParts + if (perfEntry.server_timings?.length) { + perfEntry.server_timings.forEach((timing) => { + if (isPresent(timing.duration) && !!timing.name) { + serverTimings.push({ + label: timing.name, + start: perfEntry.start_time || 0, + end: (perfEntry.start_time || 0) + timing.duration, + color: colorForSection('server_timing'), + }) + } + }) + } + + return { networkTimings: performanceParts, serverTimings } } function percentage(partDuration: number, totalDuration: number, min: number): number { @@ -239,58 +267,103 @@ function percentagesWithinEventRange({ return { startPercentage: `${partStartPercentage}%`, widthPercentage: `${partPercentage}%` } } +const TimingBar = ({ + section, + matchedSection, + rangeStart, + rangeEnd, +}: { + section: (typeof perfSections)[number] + matchedSection: EventPerformanceMeasure + rangeStart: number + rangeEnd: number +}): JSX.Element => { + const start = matchedSection.start + const end = matchedSection.end + const label = matchedSection.label || section + + const partDuration = end - start + let formattedDuration: string | undefined + let startPercentage = null + let widthPercentage = null + + if (isNaN(partDuration) || partDuration === 0) { + formattedDuration = '' + } else { + formattedDuration = humanFriendlyMilliseconds(partDuration) + const percentages = percentagesWithinEventRange({ + rangeStart, + rangeEnd, + partStart: start, + partEnd: end, + }) + startPercentage = percentages.startPercentage + widthPercentage = percentages.widthPercentage + } + + return ( + <> +
+
+ {label} +
+
+
+
+
{formattedDuration || ''}
+
+ + ) +} + const TimeLineView = ({ performanceEvent }: { performanceEvent: PerformanceEvent }): JSX.Element | null => { const rangeStart = performanceEvent.start_time const rangeEnd = performanceEvent.load_event_end ? performanceEvent.load_event_end : performanceEvent.response_end if (typeof rangeStart === 'number' && typeof rangeEnd === 'number') { - const timings = calculatePerformanceParts(performanceEvent) + const performanceMeasures = calculatePerformanceParts(performanceEvent) return (
- {perfSections.map((section) => { - const matchedSection = timings[section] - const start = matchedSection?.start - const end = matchedSection?.end - const partDuration = end - start - let formattedDuration: string | undefined - let startPercentage = null - let widthPercentage = null - - if (isNaN(partDuration) || partDuration === 0) { - formattedDuration = '' - } else { - formattedDuration = humanFriendlyMilliseconds(partDuration) - const percentages = percentagesWithinEventRange({ - rangeStart, - rangeEnd, - partStart: start, - partEnd: end, - }) - startPercentage = percentages.startPercentage - widthPercentage = percentages.widthPercentage - } - - return ( - <> -
-
- {section} -
-
-
-
-
{formattedDuration || ''}
-
- - ) - })} + {perfSections + .filter((x) => x != 'server_timing') + .map((section) => { + const matchedSection = performanceMeasures.networkTimings[section] + return matchedSection ? ( + + ) : null + })} + {performanceMeasures['serverTimings'].length > 0 ? ( + <> + + +

Server timings

+
+ {performanceMeasures.serverTimings.map((timing) => { + return timing ? ( + + ) : null + })} + + ) : null}
) } diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts index ceb129e8d8e9aa..67af763aee1004 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/calculatePerformanceParts.test.ts @@ -2,6 +2,8 @@ import { InitiatorType } from 'posthog-js' import { calculatePerformanceParts } from 'scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming' import { mapRRWebNetworkRequest } from 'scenes/session-recordings/player/inspector/performance-event-utils' +import { PerformanceEvent } from '~/types' + jest.mock('lib/colors', () => { return { getSeriesColor: jest.fn(() => '#000000'), @@ -44,7 +46,9 @@ describe('calculatePerformanceParts', () => { current_url: 'http://localhost:8000/insights', } - expect(calculatePerformanceParts(perfEvent)).toEqual({ + const performanceMeasures = calculatePerformanceParts(perfEvent) + expect(performanceMeasures.serverTimings).toEqual([]) + expect(performanceMeasures.networkTimings).toEqual({ 'request queuing time': { color: '#000000', end: 9803.099999964237, @@ -112,7 +116,9 @@ describe('calculatePerformanceParts', () => { responseBody: '�PNGblah', } const mappedToPerfEvent = mapRRWebNetworkRequest(gravatarReqRes, 'windowId', 1700296066652) - expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + const performanceMeasures = calculatePerformanceParts(mappedToPerfEvent) + expect(performanceMeasures.serverTimings).toEqual([]) + expect(performanceMeasures.networkTimings).toEqual({ // 'app cache' not included - end would be before beginning // 'connection time' has 0 length // 'dns lookup' has 0 length @@ -161,7 +167,82 @@ describe('calculatePerformanceParts', () => { isInitial: true, } const mappedToPerfEvent = mapRRWebNetworkRequest(tlsFreeReqRes, 'windowId', 1700319068449) - expect(calculatePerformanceParts(mappedToPerfEvent)).toEqual({ + const performanceMeasures = calculatePerformanceParts(mappedToPerfEvent) + expect(performanceMeasures.serverTimings).toEqual([]) + expect(performanceMeasures.networkTimings).toEqual({ + 'app cache': { + color: '#000000', + end: 6648.800000011921, + start: 6647.699999988079, + }, + 'connection time': { + color: '#000000', + end: 6649.300000011921, + start: 6648.800000011921, + }, + 'waiting for first byte': { + color: '#000000', + end: 6740.800000011921, + start: 6649.5, + }, + 'receiving response': { + color: '#000000', + end: 6741.100000023842, + start: 6740.800000011921, + }, + 'request queuing time': { + color: '#000000', + end: 6649.5, + start: 6649.300000011921, + }, + }) + }) + + it('can map server timings', () => { + const tlsFreeReqRes = { + name: 'http://localhost:8000/decide/?v=3&ip=1&_=1700319068450&ver=1.91.1', + entryType: 'resource', + startTime: 6648, + duration: 93.40000003576279, + initiatorType: 'xmlhttprequest' as InitiatorType, + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 6647.699999988079, + domainLookupStart: 6648.800000011921, + domainLookupEnd: 6648.800000011921, + connectStart: 6648.800000011921, + secureConnectionStart: 0, + connectEnd: 6649.300000011921, + requestStart: 6649.5, + responseStart: 6740.800000011921, + firstInterimResponseStart: 0, + responseEnd: 6741.100000023842, + transferSize: 2383, + encodedBodySize: 2083, + decodedBodySize: 2083, + responseStatus: 200, + endTime: 6741, + timeOrigin: 1700319061802, + timestamp: 1700319068449, + isInitial: true, + } + const mappedToPerfEvent = mapRRWebNetworkRequest(tlsFreeReqRes, 'windowId', 1700319068449) + mappedToPerfEvent.server_timings = [ + { name: 'cache', start_time: 123, duration: 0.1 } as unknown as PerformanceEvent, + { name: 'app', start_time: 123, duration: 0.2 } as unknown as PerformanceEvent, + { name: 'db', start_time: 123, duration: 0.3 } as unknown as PerformanceEvent, + ] + const performanceMeasures = calculatePerformanceParts(mappedToPerfEvent) + expect(performanceMeasures.serverTimings).toEqual([ + { color: '#000000', end: 6648.1, label: 'cache', start: 6648 }, + { color: '#000000', end: 6648.2, label: 'app', start: 6648 }, + { color: '#000000', end: 6648.3, label: 'db', start: 6648 }, + ]) + expect(performanceMeasures.networkTimings).toEqual({ 'app cache': { color: '#000000', end: 6648.800000011921, diff --git a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.test.ts b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.test.ts new file mode 100644 index 00000000000000..a1d9685ea9e964 --- /dev/null +++ b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.test.ts @@ -0,0 +1,286 @@ +import { matchNetworkEvents } from 'scenes/session-recordings/player/inspector/performance-event-utils' + +const aSingleSnapshotWithNetworkPayloads = { + windowId: '018d5247-079c-7126-8e43-464605576a62', + type: 6, + data: { + plugin: 'rrweb/network@1', + payload: { + requests: [ + // a page navigation + { + name: 'http://localhost:8000/project/1/replay/recent?sessionRecordingId=018d51f0-b624-7b16-964d-f1109f28b3da', + entryType: 'navigation', + startTime: 0, + duration: 5704.5999999940395, + initiatorType: 'navigation', + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 2.699999988079071, + domainLookupStart: 2.699999988079071, + domainLookupEnd: 2.699999988079071, + connectStart: 2.699999988079071, + secureConnectionStart: 0, + connectEnd: 2.699999988079071, + requestStart: 9.699999988079071, + responseStart: 4818.5999999940395, + firstInterimResponseStart: 0, + responseEnd: 4891.9000000059605, + transferSize: 82597, + encodedBodySize: 82297, + decodedBodySize: 82297, + responseStatus: 200, + serverTiming: [], + unloadEventStart: 4907.199999988079, + unloadEventEnd: 4907.199999988079, + domInteractive: 5515.199999988079, + domContentLoadedEventStart: 5515.199999988079, + domContentLoadedEventEnd: 5515.199999988079, + domComplete: 5703.799999982119, + loadEventStart: 5704.5, + loadEventEnd: 5704.5999999940395, + type: 'reload', + redirectCount: 0, + activationStart: 0, + criticalCHRestart: 0, + endTime: 4892, + timeOrigin: 1706482394939, + timestamp: 1706482394939, + isInitial: true, + }, + // a resoutce + { + name: 'http://localhost:8234/static/index.css', + entryType: 'resource', + startTime: 5445, + duration: 72.09999999403954, + initiatorType: 'link', + deliveryType: '', + nextHopProtocol: '', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 5445.199999988079, + domainLookupStart: 0, + domainLookupEnd: 0, + connectStart: 0, + secureConnectionStart: 0, + connectEnd: 0, + requestStart: 0, + responseStart: 0, + firstInterimResponseStart: 0, + responseEnd: 5517.299999982119, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + responseStatus: 0, + serverTiming: [], + endTime: 5517, + timeOrigin: 1706482394939, + timestamp: 1706482400384, + isInitial: true, + }, + // fetch + { + name: 'http://localhost:8000/decide/?v=3&ip=1&_=1706482405545&ver=1.103.0', + entryType: 'resource', + startTime: 10606, + duration: 2524.300000011921, + initiatorType: 'xmlhttprequest', + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 10605.799999982119, + domainLookupStart: 10605.799999982119, + domainLookupEnd: 10605.799999982119, + connectStart: 10605.799999982119, + secureConnectionStart: 0, + connectEnd: 10605.799999982119, + requestStart: 12689, + responseStart: 13084.40000000596, + firstInterimResponseStart: 0, + responseEnd: 13130.09999999404, + transferSize: 3311, + encodedBodySize: 3011, + decodedBodySize: 3011, + responseStatus: 200, + serverTiming: [], + endTime: 13130, + timeOrigin: 1706482394938, + timestamp: 1706482405543, + isInitial: true, + }, + // fetch which matches server timings + { + name: 'http://localhost:8000/api/projects/1/activity_log/important_changes?unread=true', + entryType: 'resource', + startTime: 10640, + duration: 2521.800000011921, + initiatorType: 'fetch', + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 10640.09999999404, + domainLookupStart: 12721.799999982119, + domainLookupEnd: 12721.90000000596, + connectStart: 12721.90000000596, + secureConnectionStart: 0, + connectEnd: 12722.59999999404, + requestStart: 12722.699999988079, + responseStart: 13161.299999982119, + firstInterimResponseStart: 0, + responseEnd: 13161.90000000596, + transferSize: 947, + encodedBodySize: 647, + decodedBodySize: 3337, + responseStatus: 200, + serverTiming: [{}, {}, {}, {}, {}], + endTime: 13162, + timeOrigin: 1706482394938, + timestamp: 1706482405578, + isInitial: true, + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405578, + startTime: 10640, + name: 'gather_query_parts', + duration: 223.65, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405578, + startTime: 10640, + name: 'query_for_candidate_ids', + duration: 42.63, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405578, + startTime: 10640, + name: 'construct_query', + duration: 3.75, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405578, + startTime: 10640, + name: 'query_for_data', + duration: 0.08, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405578, + startTime: 10640, + name: 'serialize', + duration: 39.98, + entryType: 'serverTiming', + }, + // another fetch which matches different server timings + { + name: 'http://localhost:8000/api/projects/1/session_recordings?session_recording_duration=%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A1%2C%22operator%22%3A%22gt%22%7D&properties=%5B%5D&events=%5B%5D&actions=%5B%5D&date_from=-7d&console_logs=%5B%5D&person_uuid=&limit=20', + entryType: 'resource', + startTime: 10654, + duration: 2927.100000023842, + initiatorType: 'fetch', + deliveryType: '', + nextHopProtocol: 'http/1.1', + renderBlockingStatus: 'non-blocking', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 10654.299999982119, + domainLookupStart: 10654.299999982119, + domainLookupEnd: 10654.299999982119, + connectStart: 10654.299999982119, + secureConnectionStart: 0, + connectEnd: 10654.299999982119, + requestStart: 12803.699999988079, + responseStart: 13577, + firstInterimResponseStart: 0, + responseEnd: 13581.40000000596, + transferSize: 3089, + encodedBodySize: 2789, + decodedBodySize: 61551, + responseStatus: 200, + serverTiming: [{}, {}, {}], + endTime: 13581, + timeOrigin: 1706482394938, + timestamp: 1706482405592, + isInitial: true, + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405592, + startTime: 10654, + name: 'load_recordings_from_clickhouse', + duration: 384.21, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405592, + startTime: 10654, + name: 'load_persons', + duration: 0.72, + entryType: 'serverTiming', + }, + { + timeOrigin: 1706482394938, + timestamp: 1706482405592, + startTime: 10654, + name: 'process_persons', + duration: 7.12, + entryType: 'serverTiming', + }, + ], + }, + }, + timestamp: 1706482463172, +} + +const someData = { + '018d5247-079c-7126-8e43-464605576a62': [aSingleSnapshotWithNetworkPayloads], +} + +describe('performance-event-utils', () => { + it('can map network events containing server timings', () => { + expect(aSingleSnapshotWithNetworkPayloads.data.payload.requests.map((r) => r.entryType)).toEqual([ + 'navigation', + 'resource', + 'resource', + 'resource', + 'serverTiming', + 'serverTiming', + 'serverTiming', + 'serverTiming', + 'serverTiming', + 'resource', + 'serverTiming', + 'serverTiming', + 'serverTiming', + ]) + // there are 13 requests in the sample data + // only 5 should remain after collapsing server timings + const actual = matchNetworkEvents(someData) + // we're collapsing server timings into their parent, so we'll have no top-level server timings + expect(actual.map((a) => a.entry_type)).toEqual(['navigation', 'resource', 'resource', 'resource', 'resource']) + + expect(actual).toMatchSnapshot() + }) +}) diff --git a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts index 502b85434f9e23..cda1b76bf125eb 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts +++ b/frontend/src/scenes/session-recordings/player/inspector/performance-event-utils.ts @@ -150,9 +150,6 @@ export function matchNetworkEvents(snapshotsByWindowId: Record { snapshots.forEach((snapshot: eventWithTime) => { if ( @@ -185,8 +182,33 @@ export function matchNetworkEvents(snapshotsByWindowId: Record { - const data: PerformanceEvent = mapRRWebNetworkRequest(capturedRequest, windowId, snapshot.timestamp) + const serverTimings: Record = {} + + const perfEvents = payload.requests.map((capturedRequest: CapturedNetworkRequest) => { + return mapRRWebNetworkRequest(capturedRequest, windowId, snapshot.timestamp) + }) + + // first find all server timings and store them by timestamp + perfEvents.forEach((perfEvent: PerformanceEvent) => { + if (perfEvent.entry_type === 'serverTiming') { + if (perfEvent.timestamp in serverTimings) { + serverTimings[perfEvent.timestamp].push(perfEvent) + } else { + serverTimings[perfEvent.timestamp] = [perfEvent] + } + } + }) + + // so we can match them to their parent events + perfEvents.forEach((data: PerformanceEvent) => { + if (data.entry_type === 'serverTiming') { + return + } + + if (data.timestamp in serverTimings) { + data.server_timings = serverTimings[data.timestamp] + delete serverTimings[data.timestamp] + } rrwebEvents.push(data) }) diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts index 054f3ced2c7257..77f82b9fa1c970 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts @@ -1,7 +1,10 @@ import { expectLogic } from 'kea-test-utils' import { api, MOCK_TEAM_ID } from 'lib/api.mock' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { convertSnapshotsByWindowId } from 'scenes/session-recordings/__mocks__/recording_snapshots' +import { + convertSnapshotsByWindowId, + snapshotsAsRealTimeJSONPayload, +} from 'scenes/session-recordings/__mocks__/recording_snapshots' import { prepareRecordingSnapshots, sessionRecordingDataLogic, @@ -13,7 +16,8 @@ import { resumeKeaLoadersErrors, silenceKeaLoadersErrors } from '~/initKea' import { useAvailableFeatures } from '~/mocks/features' import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' -import { AvailableFeature, SessionRecordingSnapshotSource } from '~/types' +import { waitForExpect } from '~/test/waitForExpect' +import { AvailableFeature, RecordingSnapshot, SessionRecordingSnapshotSource } from '~/types' import recordingEventsJson from '../__mocks__/recording_events_query' import recordingMetaJson from '../__mocks__/recording_meta.json' @@ -48,8 +52,8 @@ describe('sessionRecordingDataLogic', () => { if (req.url.searchParams.get('source') === 'blob') { return res(ctx.text(snapshotsAsJSONLines())) } else if (req.url.searchParams.get('source') === 'realtime') { - // ... since this is fake, we'll just return the same data - return res(ctx.text(snapshotsAsJSONLines())) + // ... since this is fake, we'll just return the same data in the right format + return res(ctx.json(snapshotsAsRealTimeJSONPayload())) } // with no source requested should return sources @@ -71,7 +75,11 @@ describe('sessionRecordingDataLogic', () => { }, }) initKeaTests() - logic = sessionRecordingDataLogic({ sessionRecordingId: '2' }) + logic = sessionRecordingDataLogic({ + sessionRecordingId: '2', + // we don't want to wait for the default real time polling interval in tests + realTimePollingIntervalMilliseconds: 10, + }) logic.mount() // Most of these tests assume the metadata is being loaded upfront which is the typical case logic.actions.loadRecordingMeta() @@ -175,7 +183,11 @@ describe('sessionRecordingDataLogic', () => { initKeaTests() useAvailableFeatures([]) initKeaTests() - logic = sessionRecordingDataLogic({ sessionRecordingId: '2' }) + logic = sessionRecordingDataLogic({ + sessionRecordingId: '2', + // we don't want to wait for the default real time polling interval in tests + realTimePollingIntervalMilliseconds: 10, + }) logic.mount() logic.actions.loadRecordingMeta() await expectLogic(logic).toFinishAllListeners() @@ -268,6 +280,30 @@ describe('sessionRecordingDataLogic', () => { expect(prepareRecordingSnapshots(snapshots)).toEqual(prepareRecordingSnapshots(snapshotsWithDuplicates)) }) + it('should cope with two not duplicate snapshots with the same timestamp and delay', () => { + // these two snapshots are not duplicates but have the same timestamp and delay + // this regression test proves that we deduplicate them against themselves + // prior to https://github.com/PostHog/posthog/pull/20019 + // each time prepareRecordingSnapshots was called with this input + // the result would be one event longer, introducing, instead of removing, a duplicate + const verySimilarSnapshots: RecordingSnapshot[] = [ + { + windowId: '1', + type: 3, + data: { source: 2, type: 0, id: 33, x: 852.7421875, y: 133.1640625 }, + timestamp: 1682952389798, + }, + { + windowId: '1', + type: 3, + data: { source: 2, type: 2, id: 33, x: 852, y: 133, pointerType: 0 }, + timestamp: 1682952389798, + }, + ] + // we call this multiple times and pass existing data in, so we need to make sure it doesn't change + expect(prepareRecordingSnapshots(verySimilarSnapshots, verySimilarSnapshots)).toEqual(verySimilarSnapshots) + }) + it('should match snapshot', () => { const snapshots = convertSnapshotsByWindowId(sortedRecordingSnapshotsJson.snapshot_data_by_window_id) @@ -278,15 +314,17 @@ describe('sessionRecordingDataLogic', () => { describe('blob and realtime loading', () => { beforeEach(async () => { // load a different session - logic = sessionRecordingDataLogic({ sessionRecordingId: 'has-real-time-too' }) + logic = sessionRecordingDataLogic({ + sessionRecordingId: 'has-real-time-too', + // we don't want to wait for the default real time polling interval in tests + realTimePollingIntervalMilliseconds: 10, + }) logic.mount() // Most of these tests assume the metadata is being loaded upfront which is the typical case logic.actions.loadRecordingMeta() }) it('loads each source, and on success reports recording viewed', async () => { - expect(logic.cache.realtimePollingInterval).toBeUndefined() - await expectLogic(logic, () => { logic.actions.loadRecordingSnapshots() // loading the snapshots will trigger a loadRecordingSnapshotsSuccess @@ -308,7 +346,51 @@ describe('sessionRecordingDataLogic', () => { 'loadRecordingSnapshotsSuccess', // and then we report having viewed the recording 'reportViewed', + // having loaded any real time data we start polling to check for more + 'startRealTimePolling', ]) }) + + it('can start polling for snapshots', async () => { + await expectLogic(logic, () => { + logic.actions.startRealTimePolling() + }) + .toDispatchActions([ + // the action we triggered + 'startRealTimePolling', + 'pollRecordingSnapshots', // 0 + 'pollRecordingSnapshotsSuccess', + // the returned data isn't changing from our mock, + // so we'll not keep polling indefinitely + 'pollRecordingSnapshots', // 1 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 2 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 3 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 4 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 5 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 6 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 7 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 8 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 9 + 'pollRecordingSnapshotsSuccess', + 'pollRecordingSnapshots', // 10 + 'pollRecordingSnapshotsSuccess', + ]) + .toNotHaveDispatchedActions([ + // this isn't called again + 'pollRecordingSnapshots', + ]) + + await waitForExpect(() => { + expect(logic.cache.realTimePollingTimeoutID).toBeNull() + }) + }) }) }) diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index 104b282ef08a67..256b889eb926b8 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -1,7 +1,19 @@ import posthogEE from '@posthog/ee/exports' import { EventType, eventWithTime } from '@rrweb/types' import { captureException } from '@sentry/react' -import { actions, connect, defaults, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { + actions, + BreakPointFunction, + connect, + defaults, + kea, + key, + listeners, + path, + props, + reducers, + selectors, +} from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import { FEATURE_FLAGS } from 'lib/constants' @@ -30,6 +42,7 @@ import { SessionRecordingSnapshotSource, SessionRecordingType, SessionRecordingUsageType, + SnapshotSourceType, } from '~/types' import { PostHogEE } from '../../../../@posthog/ee/types' @@ -38,6 +51,11 @@ import { createSegments, mapSnapshotsToWindowId } from './utils/segmenter' const IS_TEST_MODE = process.env.NODE_ENV === 'test' const BUFFER_MS = 60000 // +- before and after start and end of a recording to query for. +const DEFAULT_REALTIME_POLLING_MILLIS = 3000 +const REALTIME_POLLING_PARAMS = toParams({ + source: SnapshotSourceType.realtime, + version: '2', +}) let postHogEEModule: PostHogEE @@ -110,30 +128,22 @@ export const prepareRecordingSnapshots = ( newSnapshots?: RecordingSnapshot[], existingSnapshots?: RecordingSnapshot[] ): RecordingSnapshot[] => { - const seenHashes: Record = {} + const seenHashes: Set = new Set() return (newSnapshots || []) .concat(existingSnapshots ? existingSnapshots ?? [] : []) .filter((snapshot) => { // For a multitude of reasons, there can be duplicate snapshots in the same recording. - // We can deduplicate by filtering out snapshots with the same timestamp and delay value (this is quite unique as a pairing) - const key = `${snapshot.timestamp}-${snapshot.delay}` + // we have to stringify the snapshot to compare it to other snapshots. + // so we can filter by storing them all in a set - if (!seenHashes[key]) { - seenHashes[key] = [snapshot] + const key = JSON.stringify(snapshot) + if (seenHashes.has(key)) { + return false } else { - // If we are looking at an identical event time, we stringify the original snapshot if not already stringified, - // Then stringify the new snapshot and compare the two. If it is the same, we can ignore it. - seenHashes[key][0] = - typeof seenHashes[key][0] === 'string' ? seenHashes[key][0] : JSON.stringify(seenHashes[key][0]) - const newSnapshot = JSON.stringify(snapshot) - if (seenHashes[key][0] === newSnapshot) { - return false - } - seenHashes[key].push(snapshot) + seenHashes.add(key) + return true } - - return true }) .sort((a, b) => a.timestamp - b.timestamp) } @@ -162,6 +172,7 @@ const generateRecordingReportDurations = ( export interface SessionRecordingDataLogicProps { sessionRecordingId: SessionRecordingId + realTimePollingIntervalMilliseconds?: number } function makeEventsQuery( @@ -246,8 +257,17 @@ export const sessionRecordingDataLogic = kea([ reportUsageIfFullyLoaded: true, persistRecording: true, maybePersistRecording: true, + startRealTimePolling: true, + pollRecordingSnapshots: true, + pollingLoadedNoNewData: true, }), reducers(() => ({ + unnecessaryPollingCount: [ + 0, + { + pollingLoadedNoNewData: (state) => state + 1, + }, + ], filters: [ {} as Partial, { @@ -270,7 +290,29 @@ export const sessionRecordingDataLogic = kea([ }, ], })), - listeners(({ values, actions, cache }) => ({ + listeners(({ values, actions, cache, props }) => ({ + pollRecordingSnapshotsSuccess: () => { + // always make sure we've cleared up the last timeout + clearTimeout(cache.realTimePollingTimeoutID) + cache.realTimePollingTimeoutID = null + + // ten is an arbitrary limit to try to avoid sending requests to our backend unnecessarily + // we could change this or add to it e.g. only poll if browser is visible to user + if (values.unnecessaryPollingCount <= 10) { + cache.realTimePollingTimeoutID = setTimeout(() => { + actions.pollRecordingSnapshots() + }, props.realTimePollingIntervalMilliseconds || DEFAULT_REALTIME_POLLING_MILLIS) + } + }, + startRealTimePolling: () => { + if (cache.realTimePollingTimeoutID) { + clearTimeout(cache.realTimePollingTimeoutID) + } + + cache.realTimePollingTimeoutID = setTimeout(() => { + actions.pollRecordingSnapshots() + }, props.realTimePollingIntervalMilliseconds || DEFAULT_REALTIME_POLLING_MILLIS) + }, maybeLoadRecordingMeta: () => { if (!values.sessionPlayerMetaDataLoading) { actions.loadRecordingMeta() @@ -303,6 +345,11 @@ export const sessionRecordingDataLogic = kea([ actions.loadRecordingSnapshots(nextSourceToLoad) } else { actions.reportViewed() + // If we have a realtime source, start polling it + const realTimeSource = sources?.find((s) => s.source === SnapshotSourceType.realtime) + if (realTimeSource) { + actions.startRealTimePolling() + } } }, loadEventsSuccess: () => { @@ -325,7 +372,6 @@ export const sessionRecordingDataLogic = kea([ }, reportViewed: async (_, breakpoint) => { const durations = generateRecordingReportDurations(cache, values) - breakpoint() // Triggered on first paint eventUsageLogic.actions.reportRecording( @@ -353,7 +399,7 @@ export const sessionRecordingDataLogic = kea([ } }, })), - loaders(({ values, props, cache }) => ({ + loaders(({ values, props, cache, actions }) => ({ sessionPlayerMetaData: { loadRecordingMeta: async (_, breakpoint) => { cache.metaStartTime = performance.now() @@ -384,6 +430,34 @@ export const sessionRecordingDataLogic = kea([ sessionPlayerSnapshotData: [ null as SessionPlayerSnapshotData | null, { + pollRecordingSnapshots: async (_, breakpoint: BreakPointFunction) => { + await breakpoint(1) // debounce + const response = await api.recordings.listSnapshots( + props.sessionRecordingId, + REALTIME_POLLING_PARAMS + ) + breakpoint() // handle out of order + + if (response.snapshots) { + const { transformed, untransformed } = await processEncodedResponse( + response.snapshots, + props, + values.sessionPlayerSnapshotData, + values.featureFlags + ) + + if (transformed.length === (values.sessionPlayerSnapshotData?.snapshots || []).length) { + actions.pollingLoadedNoNewData() + } + + return { + ...(values.sessionPlayerSnapshotData || {}), + snapshots: transformed, + untransformed_snapshots: untransformed ?? undefined, + } + } + return values.sessionPlayerSnapshotData + }, loadRecordingSnapshots: async ({ source }, breakpoint): Promise => { if (!props.sessionRecordingId) { return values.sessionPlayerSnapshotData @@ -397,7 +471,7 @@ export const sessionRecordingDataLogic = kea([ await breakpoint(1) - if (source?.source === 'blob') { + if (source?.source === SnapshotSourceType.blob) { if (!source.blob_key) { throw new Error('Missing key') } diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index abf50543ee26a5..1654632d44c434 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -16,13 +16,11 @@ import { } from 'kea' import { router } from 'kea-router' import { delay } from 'kea-test-utils' -import { windowValues } from 'kea-window-values' import { FEATURE_FLAGS } from 'lib/constants' import { now } from 'lib/dayjs' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { clamp, downloadFile, fromParamsGivenUrl } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { getBreakpoint } from 'lib/utils/responsiveUtils' import { wrapConsole } from 'lib/utils/wrapConsole' import posthog from 'posthog-js' import { RefObject } from 'react' @@ -1008,9 +1006,6 @@ export const sessionRecordingPlayerLogic = kea( } }, })), - windowValues({ - isSmallScreen: (window: Window) => window.innerWidth < getBreakpoint('md'), - }), beforeUnmount(({ values, actions, cache, props }) => { if (props.mode === SessionRecordingPlayerMode.Preview) { diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index b5589f98102d16..65bb87d716d856 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -210,4 +210,5 @@ export const urls = { notebooks: (): string => '/notebooks', notebook: (shortId: string): string => `/notebooks/${shortId}`, canvas: (): string => `/canvas`, + moveToPostHogCloud: (): string => '/move-to-cloud', } diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index 198672332e762c..caea658ae70379 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -272,7 +272,10 @@ export const WebStatsTrendTile = ({ onSegmentClick: onDeviceTilePieChartClick, }, }, - insightProps, + insightProps: { + ...insightProps, + query, + }, } }, [onWorldMapClick, insightProps]) diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index 2879aa6ab28ddd..359764e4a703b4 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -138,9 +138,9 @@ const initialDateFrom = '-7d' as string | null const initialDateTo = null as string | null const initialInterval = getDefaultInterval(initialDateFrom, initialDateTo) -const getDashboardItemId = (section: Tile, tab?: string): `new-${string}` => { - // pretend to be Adhoc as that gives the correct behaviour elsewhere - return `new-Adhoc.web-analytics.${section}${tab ? `-${tab}` : ''}` +const getDashboardItemId = (section: Tile, tab?: string): `new-AdHoc.${string}` => { + // pretend to be a new-AdHoc to get the correct behaviour elsewhere + return `new-AdHoc.web-analytics.${section}${tab ? `-${tab}` : ''}` } export const webAnalyticsLogic = kea([ path(['scenes', 'webAnalytics', 'webAnalyticsSceneLogic']), diff --git a/frontend/src/test/waitForExpect.ts b/frontend/src/test/waitForExpect.ts new file mode 100644 index 00000000000000..b55dddba813ba1 --- /dev/null +++ b/frontend/src/test/waitForExpect.ts @@ -0,0 +1,18 @@ +/** duplicated from the plugin-server + * Allows for running expectations that are expected to pass eventually. + * This is useful for, e.g. waiting for events to have been ingested into + * the database. + */ +export const waitForExpect = async (fn: () => T | Promise, timeout = 10_000, interval = 1_000): Promise => { + const start = Date.now() + while (true) { + try { + return await fn() + } catch (error) { + if (Date.now() - start > timeout) { + throw error + } + await new Promise((resolve) => setTimeout(resolve, interval)) + } + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index c0e81f5f55665f..3770b462743a78 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -754,8 +754,19 @@ export type EncodedRecordingSnapshot = { data: eventWithTime[] } +// we can duplicate the name SnapshotSourceType for the object and the type +// since one only exists to be used in the other +// this way if we want to reference one of the valid string values for SnapshotSourceType +// we have a strongly typed way to do it +export const SnapshotSourceType = { + blob: 'blob', + realtime: 'realtime', +} as const + +export type SnapshotSourceType = (typeof SnapshotSourceType)[keyof typeof SnapshotSourceType] + export interface SessionRecordingSnapshotSource { - source: 'blob' | 'realtime' + source: SnapshotSourceType start_timestamp?: string end_timestamp?: string blob_key?: string @@ -1272,6 +1283,9 @@ export interface PerformanceEvent { //rrweb/network@1 - i.e. not in ClickHouse table is_initial?: boolean raw?: Record + + //server timings - reported as separate events but added back in here on the front end + server_timings?: PerformanceEvent[] } export interface CurrentBillCycleType { @@ -3669,6 +3683,7 @@ export enum SDKKey { WORDPRESS = 'wordpress', SENTRY = 'sentry', RETOOL = 'retool', + HTML_SNIPPET = 'html', } export enum SDKTag { @@ -3705,7 +3720,6 @@ export enum SidePanelTab { Docs = 'docs', Activation = 'activation', Settings = 'settings', - Welcome = 'welcome', FeaturePreviews = 'feature-previews', Activity = 'activity', Discussion = 'discussion', diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index fa1b290c0793ac..e7d5ec8b188727 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -133,6 +133,7 @@ export function getDefaultConfig(): PluginsServerConfig { POE_WRITES_EXCLUDE_TEAMS: '', RELOAD_PLUGIN_JITTER_MAX_MS: 60000, RUSTY_HOOK_FOR_TEAMS: '', + RUSTY_HOOK_ROLLOUT_PERCENTAGE: 0, RUSTY_HOOK_URL: '', STARTUP_PROFILE_DURATION_SECONDS: 300, // 5 minutes diff --git a/plugin-server/src/main/pluginsServer.ts b/plugin-server/src/main/pluginsServer.ts index 42cdee24b3bab7..ef3b369902551b 100644 --- a/plugin-server/src/main/pluginsServer.ts +++ b/plugin-server/src/main/pluginsServer.ts @@ -361,6 +361,7 @@ export async function startPluginsServer( hub?.rustyHook ?? new RustyHook( buildIntegerMatcher(serverConfig.RUSTY_HOOK_FOR_TEAMS, true), + serverConfig.RUSTY_HOOK_ROLLOUT_PERCENTAGE, serverConfig.RUSTY_HOOK_URL, serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS ) diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index 0031ec514f39ab..d6e375fc814eb1 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -204,6 +204,7 @@ export interface PluginsServerConfig { POE_WRITES_EXCLUDE_TEAMS: string RELOAD_PLUGIN_JITTER_MAX_MS: number RUSTY_HOOK_FOR_TEAMS: string + RUSTY_HOOK_ROLLOUT_PERCENTAGE: number RUSTY_HOOK_URL: string SKIP_UPDATE_EVENT_AND_PROPERTIES_STEP: boolean diff --git a/plugin-server/src/utils/db/hub.ts b/plugin-server/src/utils/db/hub.ts index 0e14d29bf56430..1389b2a9544635 100644 --- a/plugin-server/src/utils/db/hub.ts +++ b/plugin-server/src/utils/db/hub.ts @@ -144,6 +144,7 @@ export async function createHub( const rootAccessManager = new RootAccessManager(db) const rustyHook = new RustyHook( buildIntegerMatcher(serverConfig.RUSTY_HOOK_FOR_TEAMS, true), + serverConfig.RUSTY_HOOK_ROLLOUT_PERCENTAGE, serverConfig.RUSTY_HOOK_URL, serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS ) diff --git a/plugin-server/src/worker/rusty-hook.ts b/plugin-server/src/worker/rusty-hook.ts index d71fae955db73c..208369932895f7 100644 --- a/plugin-server/src/worker/rusty-hook.ts +++ b/plugin-server/src/worker/rusty-hook.ts @@ -24,6 +24,7 @@ interface RustyWebhookPayload { export class RustyHook { constructor( private enabledForTeams: ValueMatcher, + private rolloutPercentage: number, private serviceUrl: string, private requestTimeoutMs: number ) {} @@ -39,7 +40,10 @@ export class RustyHook { pluginId: number pluginConfigId: number }): Promise { - if (!this.enabledForTeams(teamId)) { + // A simple and blunt rollout that just uses the last digits of the Team ID as a stable + // selection against the `rolloutPercentage`. + const enabledByRolloutPercentage = (teamId % 1000) / 1000 < this.rolloutPercentage + if (!enabledByRolloutPercentage && !this.enabledForTeams(teamId)) { return false } diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index 360f980c9a6059..3673329ea32d04 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -54,6 +54,9 @@ ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission, ) +from posthog.queries.base import ( + determine_parsed_date_for_property_matching, +) from posthog.rate_limit import BurstRateThrottle from loginas.utils import is_impersonated_session @@ -233,6 +236,14 @@ def properties_all_match(predicate): code="cohort_does_not_exist", ) + if prop.operator in ("is_date_before", "is_date_after"): + parsed_date = determine_parsed_date_for_property_matching(prop.value) + + if not parsed_date: + raise serializers.ValidationError( + detail=f"Invalid date value: {prop.value}", code="invalid_date" + ) + payloads = filters.get("payloads", {}) if not isinstance(payloads, dict): @@ -625,6 +636,7 @@ def user_blast_radius(self, request: request.Request, **kwargs): condition = request.data.get("condition") or {} group_type_index = request.data.get("group_type_index", None) + # TODO: Handle distinct_id and $group_key properties, which are not currently supported users_affected, total_users = get_user_blast_radius(self.team, condition, group_type_index) return Response( diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 12136ab1f72f7d..71ce40a4dc3611 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -276,12 +276,12 @@ (SELECT id FROM person WHERE team_id = 2 - AND ((has(['none'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', '')) - OR has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', '')))) ) + AND (((has(['none'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', ''))) + OR (has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(properties, 'group'), '^"|"$', ''))))) ) GROUP BY id HAVING max(is_deleted) = 0 - AND ((has(['none'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', '')) - OR has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', '')))) SETTINGS optimize_aggregation_in_order = 1) + AND (((has(['none'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', ''))) + OR (has(['1', '2', '3'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), 'group'), '^"|"$', ''))))) SETTINGS optimize_aggregation_in_order = 1) ''' # --- # name: TestBlastRadius.test_user_blast_radius_with_single_cohort.1 diff --git a/posthog/api/test/test_feature_flag.py b/posthog/api/test/test_feature_flag.py index 2fae24ecdf0712..90e2e980271a50 100644 --- a/posthog/api/test/test_feature_flag.py +++ b/posthog/api/test/test_feature_flag.py @@ -2863,6 +2863,53 @@ def test_validation_person_properties(self): }, ) + def test_create_flag_with_invalid_date(self): + resp = self._create_flag_with_properties( + "date-flag", + [ + { + "key": "created_for", + "type": "person", + "value": "6hed", + "operator": "is_date_before", + } + ], + expected_status=status.HTTP_400_BAD_REQUEST, + ) + + self.assertDictContainsSubset( + { + "type": "validation_error", + "code": "invalid_date", + "detail": "Invalid date value: 6hed", + "attr": "filters", + }, + resp.json(), + ) + + resp = self._create_flag_with_properties( + "date-flag", + [ + { + "key": "created_for", + "type": "person", + "value": "1234-02-993284", + "operator": "is_date_after", + } + ], + expected_status=status.HTTP_400_BAD_REQUEST, + ) + + self.assertDictContainsSubset( + { + "type": "validation_error", + "code": "invalid_date", + "detail": "Invalid date value: 1234-02-993284", + "attr": "filters", + }, + resp.json(), + ) + def test_creating_feature_flag_with_non_existant_cohort(self): cohort_request = self._create_flag_with_properties( "cohort-flag", @@ -4150,6 +4197,37 @@ def test_user_blast_radius(self): response_json = response.json() self.assertDictContainsSubset({"users_affected": 4, "total_users": 10}, response_json) + @freeze_time("2024-01-11") + def test_user_blast_radius_with_relative_date_filters(self): + for i in range(8): + _create_person( + team_id=self.team.pk, + distinct_ids=[f"person{i}"], + properties={"group": f"{i}", "created_at": f"2023-0{i+1}-04"}, + ) + + response = self.client.post( + f"/api/projects/{self.team.id}/feature_flags/user_blast_radius", + { + "condition": { + "properties": [ + { + "key": "created_at", + "type": "person", + "value": "-10m", + "operator": "is_date_before", + } + ], + "rollout_percentage": 100, + } + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response_json = response.json() + self.assertDictContainsSubset({"users_affected": 3, "total_users": 8}, response_json) + def test_user_blast_radius_with_zero_users(self): response = self.client.post( f"/api/projects/{self.team.id}/feature_flags/user_blast_radius", diff --git a/posthog/apps.py b/posthog/apps.py index e86d85c234eaad..710ee74bf67e8b 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -55,7 +55,7 @@ def ready(self): # load feature flag definitions if not already loaded if not posthoganalytics.disabled and posthoganalytics.feature_flag_definitions() is None: - posthoganalytics.default_client.load_feature_flags() + posthoganalytics.load_feature_flags() from posthog.async_migrations.setup import setup_async_migrations diff --git a/posthog/hogql_queries/insights/stickiness_query_runner.py b/posthog/hogql_queries/insights/stickiness_query_runner.py index 0a95c42f7e85f7..3b311789f92672 100644 --- a/posthog/hogql_queries/insights/stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/stickiness_query_runner.py @@ -256,13 +256,20 @@ def where_clause(self, series_with_extra: SeriesWithExtras) -> ast.Expr: ) # Series - if self.series_event(series) is not None: + if isinstance(series, EventsNode) and series.event is not None: filters.append( parse_expr( "event = {event}", - placeholders={"event": ast.Constant(value=self.series_event(series))}, + placeholders={"event": ast.Constant(value=series.event)}, ) ) + elif isinstance(series, ActionsNode): + try: + action = Action.objects.get(pk=int(series.id), team=self.team) + filters.append(action_to_expr(action)) + except Action.DoesNotExist: + # If an action doesn't exist, we want to return no events + filters.append(parse_expr("1 = 2")) # Filter Test Accounts if ( @@ -281,15 +288,6 @@ def where_clause(self, series_with_extra: SeriesWithExtras) -> ast.Expr: if series.properties is not None and series.properties != []: filters.append(property_to_expr(series.properties, self.team)) - # Actions - if isinstance(series, ActionsNode): - try: - action = Action.objects.get(pk=int(series.id), team=self.team) - filters.append(action_to_expr(action)) - except Action.DoesNotExist: - # If an action doesn't exist, we want to return no events - filters.append(parse_expr("1 = 2")) - if len(filters) == 0: return ast.Constant(value=True) elif len(filters) == 1: diff --git a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py index c1d217872046b5..0304184731c760 100644 --- a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py @@ -442,7 +442,7 @@ def test_any_event(self): def test_actions(self): self._create_test_events() - action = Action.objects.create(name="$pageview", team=self.team) + action = Action.objects.create(name="My Action", team=self.team) ActionStep.objects.create( action=action, event="$pageview", diff --git a/posthog/hogql_queries/legacy_compatibility/feature_flag.py b/posthog/hogql_queries/legacy_compatibility/feature_flag.py index f44153feaca9e8..2c1708223d9b96 100644 --- a/posthog/hogql_queries/legacy_compatibility/feature_flag.py +++ b/posthog/hogql_queries/legacy_compatibility/feature_flag.py @@ -1,3 +1,4 @@ +from typing import cast import posthoganalytics from django.conf import settings from posthog.cloud_utils import is_cloud @@ -16,7 +17,7 @@ def hogql_insights_enabled(user: User | AnonymousUser) -> bool: return posthoganalytics.feature_enabled( "hogql-insights", - user.distinct_id, + cast(str, user.distinct_id), person_properties={"email": user.email}, only_evaluate_locally=True, send_feature_flag_events=False, diff --git a/posthog/models/feature_flag/flag_matching.py b/posthog/models/feature_flag/flag_matching.py index 90b64805d9ac21..63054e0a957a08 100644 --- a/posthog/models/feature_flag/flag_matching.py +++ b/posthog/models/feature_flag/flag_matching.py @@ -682,6 +682,9 @@ def get_all_feature_flags( property_value_overrides: Dict[str, Union[str, int]] = {}, group_property_value_overrides: Dict[str, Dict[str, Union[str, int]]] = {}, ) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict], Dict[str, object], bool]: + property_value_overrides, group_property_value_overrides = add_local_person_and_group_properties( + distinct_id, groups, property_value_overrides, group_property_value_overrides + ) all_feature_flags = get_feature_flags_for_team_in_cache(team_id) cache_hit = True if all_feature_flags is None: @@ -954,3 +957,17 @@ def get_all_properties_with_math_operators( all_keys_and_fields.append(key_and_field_for_property(prop)) return all_keys_and_fields + + +def add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties): + all_person_properties = {"distinct_id": distinct_id, **(person_properties or {})} + + all_group_properties = {} + if groups: + for group_name in groups: + all_group_properties[group_name] = { + "$group_key": groups[group_name], + **(group_properties.get(group_name) or {}), + } + + return all_person_properties, all_group_properties diff --git a/posthog/models/feature_flag/user_blast_radius.py b/posthog/models/feature_flag/user_blast_radius.py index 5843e3513e6b16..b7e9d216b3f6c0 100644 --- a/posthog/models/feature_flag/user_blast_radius.py +++ b/posthog/models/feature_flag/user_blast_radius.py @@ -7,6 +7,19 @@ from posthog.models.filters import Filter from posthog.models.property import GroupTypeIndex from posthog.models.team.team import Team +from posthog.queries.base import relative_date_parse_for_feature_flag_matching + + +def replace_proxy_properties(team: Team, feature_flag_condition: dict): + prop_groups = Filter(data=feature_flag_condition, team=team).property_groups + + for prop in prop_groups.flat: + if prop.operator in ("is_date_before", "is_date_after"): + relative_date = relative_date_parse_for_feature_flag_matching(str(prop.value)) + if relative_date: + prop.value = relative_date.strftime("%Y-%m-%d %H:%M:%S") + + return Filter(data={"properties": prop_groups.to_dict()}, team=team) def get_user_blast_radius( @@ -19,6 +32,8 @@ def get_user_blast_radius( # No rollout % calculations here, since it makes more sense to compute that on the frontend properties = feature_flag_condition.get("properties") or [] + cleaned_filter = replace_proxy_properties(team, feature_flag_condition) + if group_type_index is not None: try: from ee.clickhouse.queries.groups_join_query import GroupsJoinQuery @@ -26,7 +41,7 @@ def get_user_blast_radius( return 0, 0 if len(properties) > 0: - filter = Filter(data=feature_flag_condition, team=team) + filter = cleaned_filter for property in filter.property_groups.flat: if property.group_type_index is None or (property.group_type_index != group_type_index): @@ -50,7 +65,7 @@ def get_user_blast_radius( return total_affected_count, team.groups_seen_so_far(group_type_index) if len(properties) > 0: - filter = Filter(data=feature_flag_condition, team=team) + filter = cleaned_filter cohort_filters = [] for property in filter.property_groups.flat: if property.type in ["cohort", "precalculated-cohort", "static-cohort"]: diff --git a/posthog/models/filters/test/__snapshots__/test_filter.ambr b/posthog/models/filters/test/__snapshots__/test_filter.ambr index 798a020dd3f355..cbc6a06873d87f 100644 --- a/posthog/models/filters/test/__snapshots__/test_filter.ambr +++ b/posthog/models/filters/test/__snapshots__/test_filter.ambr @@ -359,7 +359,7 @@ INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") WHERE ("posthog_persondistinctid"."distinct_id" = 'example_id' AND "posthog_person"."team_id" = 2 - AND "posthog_person"."id" = -1) + AND ("posthog_person"."properties" -> 'created_at') > '["2m", "3d"]') LIMIT 1 ''' # --- @@ -370,7 +370,7 @@ INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") WHERE ("posthog_persondistinctid"."distinct_id" = 'example_id' AND "posthog_person"."team_id" = 2 - AND "posthog_person"."id" = -1) + AND ("posthog_person"."properties" -> 'created_at') > '"bazinga"') LIMIT 1 ''' # --- diff --git a/posthog/models/filters/test/test_filter.py b/posthog/models/filters/test/test_filter.py index 4dcfe35556058d..08465aa9d9c4ca 100644 --- a/posthog/models/filters/test/test_filter.py +++ b/posthog/models/filters/test/test_filter.py @@ -781,11 +781,7 @@ def test_person_relative_date_parsing(self): properties={"created_at": "2021-04-04T12:00:00Z"}, ) filter = Filter( - data={ - "properties": [ - {"key": "created_at", "value": "2d", "type": "person", "operator": "is_relative_date_after"} - ] - } + data={"properties": [{"key": "created_at", "value": "2d", "type": "person", "operator": "is_date_after"}]} ) with self.assertNumQueries(1), freeze_time("2021-04-06T10:00:00"): @@ -807,11 +803,7 @@ def test_person_relative_date_parsing_with_override_property(self): properties={"created_at": "2021-04-04T12:00:00Z"}, ) filter = Filter( - data={ - "properties": [ - {"key": "created_at", "value": "2m", "type": "person", "operator": "is_relative_date_after"} - ] - } + data={"properties": [{"key": "created_at", "value": "2m", "type": "person", "operator": "is_date_after"}]} ) with self.assertNumQueries(1): @@ -840,7 +832,7 @@ def test_person_relative_date_parsing_with_invalid_date(self): filter = Filter( data={ "properties": [ - {"key": "created_at", "value": ["2m", "3d"], "type": "person", "operator": "is_relative_date_after"} + {"key": "created_at", "value": ["2m", "3d"], "type": "person", "operator": "is_date_after"} ] } ) @@ -859,9 +851,7 @@ def test_person_relative_date_parsing_with_invalid_date(self): filter = Filter( data={ - "properties": [ - {"key": "created_at", "value": "bazinga", "type": "person", "operator": "is_relative_date_after"} - ] + "properties": [{"key": "created_at", "value": "bazinga", "type": "person", "operator": "is_date_after"}] } ) diff --git a/posthog/models/property/property.py b/posthog/models/property/property.py index 0db92dcf7fc174..dee63194ba41e0 100644 --- a/posthog/models/property/property.py +++ b/posthog/models/property/property.py @@ -71,8 +71,6 @@ class BehavioralPropertyType(str, Enum): "is_date_exact", "is_date_after", "is_date_before", - "is_relative_date_after", - "is_relative_date_before", ] OperatorInterval = Literal["day", "week", "month", "year"] diff --git a/posthog/models/property/util.py b/posthog/models/property/util.py index cfefbb94dc1f7e..c55862ef3c6b05 100644 --- a/posthog/models/property/util.py +++ b/posthog/models/property/util.py @@ -397,8 +397,6 @@ def negate_operator(operator: OperatorType) -> OperatorType: "is_not_set": "is_set", "is_date_before": "is_date_after", "is_date_after": "is_date_before", - "is_relative_date_before": "is_relative_date_after", - "is_relative_date_after": "is_relative_date_before", # is_date_exact not yet supported }.get(operator, operator) # type: ignore diff --git a/posthog/queries/base.py b/posthog/queries/base.py index b0b559fc7fcaa7..d6db0083a3eff4 100644 --- a/posthog/queries/base.py +++ b/posthog/queries/base.py @@ -172,27 +172,20 @@ def compare(lhs, rhs, operator): else: return compare(str(override_value), str(value), operator) - if operator in ["is_date_before", "is_date_after", "is_relative_date_before", "is_relative_date_after"]: - try: - if operator in ["is_relative_date_before", "is_relative_date_after"]: - parsed_date = relative_date_parse_for_feature_flag_matching(str(value)) - else: - parsed_date = parser.parse(str(value)) - parsed_date = convert_to_datetime_aware(parsed_date) - except Exception: - return False + if operator in ["is_date_before", "is_date_after"]: + parsed_date = determine_parsed_date_for_property_matching(value) if not parsed_date: return False if isinstance(override_value, datetime.datetime): override_date = convert_to_datetime_aware(override_value) - if operator in ("is_date_before", "is_relative_date_before"): + if operator == "is_date_before": return override_date < parsed_date else: return override_date > parsed_date elif isinstance(override_value, datetime.date): - if operator in ("is_date_before", "is_relative_date_before"): + if operator == "is_date_before": return override_value < parsed_date.date() else: return override_value > parsed_date.date() @@ -200,7 +193,7 @@ def compare(lhs, rhs, operator): try: override_date = parser.parse(override_value) override_date = convert_to_datetime_aware(override_date) - if operator in ("is_date_before", "is_relative_date_before"): + if operator == "is_date_before": return override_date < parsed_date else: return override_date > parsed_date @@ -210,6 +203,20 @@ def compare(lhs, rhs, operator): return False +def determine_parsed_date_for_property_matching(value: ValueT): + parsed_date = None + try: + parsed_date = relative_date_parse_for_feature_flag_matching(str(value)) + + if not parsed_date: + parsed_date = parser.parse(str(value)) + parsed_date = convert_to_datetime_aware(parsed_date) + except Exception: + return None + + return parsed_date + + def empty_or_null_with_value_q( column: str, key: str, @@ -349,16 +356,13 @@ def property_to_Q( negated=True, ) - if property.operator in ("is_date_after", "is_date_before", "is_relative_date_before", "is_relative_date_after"): - effective_operator = "gt" if property.operator in ("is_date_after", "is_relative_date_after") else "lt" + if property.operator in ("is_date_after", "is_date_before"): + effective_operator = "gt" if property.operator == "is_date_after" else "lt" effective_value = value - if property.operator in ("is_relative_date_before", "is_relative_date_after"): - relative_date = relative_date_parse_for_feature_flag_matching(str(value)) - if relative_date: - effective_value = relative_date.isoformat() - else: - # Return no data for invalid relative dates - return Q(pk=-1) + + relative_date = relative_date_parse_for_feature_flag_matching(str(value)) + if relative_date: + effective_value = relative_date.isoformat() return Q(**{f"{column}__{property.key}__{effective_operator}": effective_value}) @@ -444,7 +448,7 @@ def is_truthy_or_falsy_property_value(value: Any) -> bool: def relative_date_parse_for_feature_flag_matching(value: str) -> Optional[datetime.datetime]: - regex = r"^(?P[0-9]+)(?P[a-z])$" + regex = r"^-?(?P[0-9]+)(?P[a-z])$" match = re.search(regex, value) parsed_dt = datetime.datetime.now(tz=ZoneInfo("UTC")) if match: diff --git a/posthog/queries/test/test_base.py b/posthog/queries/test/test_base.py index a3c4dd9f3a3abf..a363b26ce19bcc 100644 --- a/posthog/queries/test/test_base.py +++ b/posthog/queries/test/test_base.py @@ -250,7 +250,7 @@ def test_match_property_date_operators(self): @freeze_time("2022-05-01") def test_match_property_relative_date_operators(self): - property_a = Property(key="key", value="6h", operator="is_relative_date_before") + property_a = Property(key="key", value="6h", operator="is_date_before") self.assertTrue(match_property(property_a, {"key": "2022-03-01"})) self.assertTrue(match_property(property_a, {"key": "2022-04-30"})) self.assertTrue(match_property(property_a, {"key": datetime.datetime(2022, 4, 30, 1, 2, 3)})) @@ -273,7 +273,7 @@ def test_match_property_relative_date_operators(self): # can't be invalid string self.assertFalse(match_property(property_a, {"key": "abcdef"})) - property_b = Property(key="key", value="1h", operator="is_relative_date_after") + property_b = Property(key="key", value="1h", operator="is_date_after") self.assertTrue(match_property(property_b, {"key": "2022-05-02"})) self.assertTrue(match_property(property_b, {"key": "2022-05-30"})) self.assertTrue(match_property(property_b, {"key": datetime.datetime(2022, 5, 30)})) @@ -284,13 +284,13 @@ def test_match_property_relative_date_operators(self): self.assertFalse(match_property(property_b, {"key": "abcdef"})) # Invalid flag property - property_c = Property(key="key", value=1234, operator="is_relative_date_after") + property_c = Property(key="key", value=1234, operator="is_date_after") self.assertFalse(match_property(property_c, {"key": 1})) - self.assertFalse(match_property(property_c, {"key": "2022-05-30"})) + self.assertTrue(match_property(property_c, {"key": "2022-05-30"})) # # Timezone aware property - property_d = Property(key="key", value="12d", operator="is_relative_date_before") + property_d = Property(key="key", value="12d", operator="is_date_before") self.assertFalse(match_property(property_d, {"key": "2022-05-30"})) self.assertTrue(match_property(property_d, {"key": "2022-03-30"})) @@ -300,45 +300,45 @@ def test_match_property_relative_date_operators(self): self.assertFalse(match_property(property_d, {"key": "2022-04-19 02:00:01+02:00"})) # Try all possible relative dates - property_e = Property(key="key", value="1h", operator="is_relative_date_before") + property_e = Property(key="key", value="1h", operator="is_date_before") self.assertFalse(match_property(property_e, {"key": "2022-05-01 00:00:00"})) self.assertTrue(match_property(property_e, {"key": "2022-04-30 22:00:00"})) - property_f = Property(key="key", value="1d", operator="is_relative_date_before") + property_f = Property(key="key", value="-1d", operator="is_date_before") self.assertTrue(match_property(property_f, {"key": "2022-04-29 23:59:00"})) self.assertFalse(match_property(property_f, {"key": "2022-04-30 00:00:01"})) - property_g = Property(key="key", value="1w", operator="is_relative_date_before") + property_g = Property(key="key", value="1w", operator="is_date_before") self.assertTrue(match_property(property_g, {"key": "2022-04-23 00:00:00"})) self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:00"})) self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:01"})) - property_h = Property(key="key", value="1m", operator="is_relative_date_before") + property_h = Property(key="key", value="1m", operator="is_date_before") self.assertTrue(match_property(property_h, {"key": "2022-03-01 00:00:00"})) self.assertFalse(match_property(property_h, {"key": "2022-04-05 00:00:00"})) - property_i = Property(key="key", value="1y", operator="is_relative_date_before") + property_i = Property(key="key", value="1y", operator="is_date_before") self.assertTrue(match_property(property_i, {"key": "2021-04-28 00:00:00"})) self.assertFalse(match_property(property_i, {"key": "2021-05-01 00:00:01"})) - property_j = Property(key="key", value="122h", operator="is_relative_date_after") + property_j = Property(key="key", value="-122h", operator="is_date_after") self.assertTrue(match_property(property_j, {"key": "2022-05-01 00:00:00"})) self.assertFalse(match_property(property_j, {"key": "2022-04-23 01:00:00"})) - property_k = Property(key="key", value="2d", operator="is_relative_date_after") + property_k = Property(key="key", value="2d", operator="is_date_after") self.assertTrue(match_property(property_k, {"key": "2022-05-01 00:00:00"})) self.assertTrue(match_property(property_k, {"key": "2022-04-29 00:00:01"})) self.assertFalse(match_property(property_k, {"key": "2022-04-29 00:00:00"})) - property_l = Property(key="key", value="02w", operator="is_relative_date_after") + property_l = Property(key="key", value="02w", operator="is_date_after") self.assertTrue(match_property(property_l, {"key": "2022-05-01 00:00:00"})) self.assertFalse(match_property(property_l, {"key": "2022-04-16 00:00:00"})) - property_m = Property(key="key", value="1m", operator="is_relative_date_after") + property_m = Property(key="key", value="-1m", operator="is_date_after") self.assertTrue(match_property(property_m, {"key": "2022-04-01 00:00:01"})) self.assertFalse(match_property(property_m, {"key": "2022-04-01 00:00:00"})) - property_n = Property(key="key", value="1y", operator="is_relative_date_after") + property_n = Property(key="key", value="1y", operator="is_date_after") self.assertTrue(match_property(property_n, {"key": "2022-05-01 00:00:00"})) self.assertTrue(match_property(property_n, {"key": "2021-05-01 00:00:01"})) self.assertFalse(match_property(property_n, {"key": "2021-05-01 00:00:00"})) diff --git a/posthog/test/test_feature_flag.py b/posthog/test/test_feature_flag.py index d1056f26e57224..db0364b3007d88 100644 --- a/posthog/test/test_feature_flag.py +++ b/posthog/test/test_feature_flag.py @@ -4925,7 +4925,7 @@ def test_relative_date_operator(self): { "key": "date_1", "value": "6h", - "operator": "is_relative_date_before", + "operator": "is_date_before", "type": "person", }, ] @@ -4943,7 +4943,7 @@ def test_relative_date_operator(self): { "key": "date_3", "value": "2d", - "operator": "is_relative_date_after", + "operator": "is_date_after", "type": "person", }, ] @@ -4961,7 +4961,7 @@ def test_relative_date_operator(self): { "key": "date_3", "value": "2h", - "operator": "is_relative_date_after", + "operator": "is_date_after", "type": "person", }, ] @@ -4979,7 +4979,7 @@ def test_relative_date_operator(self): { "key": "date_invalid", "value": "2h", - "operator": "is_relative_date_after", + "operator": "is_date_after", "type": "person", }, ] @@ -4997,7 +4997,7 @@ def test_relative_date_operator(self): { "key": "date_1", "value": "bazinga", - "operator": "is_relative_date_after", + "operator": "is_date_after", "type": "person", }, ] diff --git a/requirements.in b/requirements.in index 96f96ca463ad4b..8512f7896536ad 100644 --- a/requirements.in +++ b/requirements.in @@ -59,7 +59,7 @@ parso==0.8.1 pexpect==4.7.0 pickleshare==0.7.5 Pillow==10.2.0 -posthoganalytics==3.0.1 +posthoganalytics==3.3.4 prance==23.06.21.0 psycopg2-binary==2.9.7 psycopg==3.1.13 diff --git a/requirements.txt b/requirements.txt index 96daa93eeb640f..ca9b1feec7f942 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ aioboto3==12.0.0 aiobotocore[boto3]==2.7.0 # via # aioboto3 - # aiobotocore # s3fs aiohttp==3.9.0 # via @@ -253,7 +252,6 @@ giturlparse==0.12.0 # via dlt google-api-core[grpc]==2.11.1 # via - # google-api-core # google-cloud-bigquery # google-cloud-core google-auth==2.22.0 @@ -419,7 +417,7 @@ pillow==10.2.0 # via -r requirements.in ply==3.11 # via jsonpath-ng -posthoganalytics==3.0.1 +posthoganalytics==3.3.4 # via -r requirements.in prance==23.6.21.0 # via -r requirements.in @@ -683,7 +681,6 @@ urllib3[secure,socks]==1.26.18 # selenium # sentry-sdk # snowflake-connector-python - # urllib3 urllib3-secure-extra==0.1.0 # via urllib3 vine==5.0.0