diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index a172b05264d4..693e17986649 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -31,7 +31,8 @@ if (container) { background: container.dataset.background, isDbip: container.dataset.isDbip === 'true', flags: JSON.parse(container.dataset.flags), - validIntervalsByPeriod: JSON.parse(container.dataset.validIntervalsByPeriod) + validIntervalsByPeriod: JSON.parse(container.dataset.validIntervalsByPeriod), + shared: !!container.dataset.sharedLinkAuth, } const loggedIn = container.dataset.loggedIn === 'true' diff --git a/assets/js/dashboard/filters.js b/assets/js/dashboard/filters.js index 62bc2d2d9117..0b26a158dc34 100644 --- a/assets/js/dashboard/filters.js +++ b/assets/js/dashboard/filters.js @@ -84,7 +84,7 @@ function filterDropdownOption(site, option) { {({ active }) => ( filterDropdownOption(site, option)) @@ -193,7 +193,7 @@ function Filters(props) { title={`Edit filter: ${formattedFilters[type]}`} className="flex w-full h-full items-center py-2 pl-3" to={{ - pathname: `/${encodeURIComponent(site.domain)}/filter/${FILTER_GROUP_TO_MODAL_TYPE[type]}`, + pathname: `/filter/${FILTER_GROUP_TO_MODAL_TYPE[type]}`, search: window.location.search }} > @@ -231,7 +231,7 @@ function Filters(props) { } function trackFilterMenu() { - window.plausible && window.plausible('Filter Menu: Open', {u: `${window.location.protocol}//${window.location.hostname}/:dashboard`}) + window.plausible && window.plausible('Filter Menu: Open', { u: `${window.location.protocol}//${window.location.hostname}/:dashboard` }) } function renderDropDown() { diff --git a/assets/js/dashboard/router.js b/assets/js/dashboard/router.js index 4605c9fd15c6..8f591f62249d 100644 --- a/assets/js/dashboard/router.js +++ b/assets/js/dashboard/router.js @@ -28,50 +28,50 @@ function ScrollToTop() { export default function Router({ site, loggedIn, currentUserRole }) { return ( - - + + - + - + - + - + - + - + - - + + - + - + - + - + - + - + ); } diff --git a/assets/js/dashboard/stats/behaviours/conversions.js b/assets/js/dashboard/stats/behaviours/conversions.js index c474b5786714..6161b9457f14 100644 --- a/assets/js/dashboard/stats/behaviours/conversions.js +++ b/assets/js/dashboard/stats/behaviours/conversions.js @@ -34,7 +34,7 @@ export default function Conversions(props) { BUILD_EXTRA && { name: 'total_revenue', label: 'Revenue', hiddenOnMobile: true }, BUILD_EXTRA && { name: 'average_revenue', label: 'Average', hiddenOnMobile: true } ]} - detailsLink={url.sitePath(site, '/conversions')} + detailsLink={url.sitePath('conversions')} maybeHideDetails={true} query={query} color="bg-red-50" diff --git a/assets/js/dashboard/stats/behaviours/goal-conversions.js b/assets/js/dashboard/stats/behaviours/goal-conversions.js index 516ed11ae164..0996e33d25cb 100644 --- a/assets/js/dashboard/stats/behaviours/goal-conversions.js +++ b/assets/js/dashboard/stats/behaviours/goal-conversions.js @@ -64,7 +64,7 @@ function SpecialPropBreakdown(props) { { name: 'events', label: 'Events', hiddenOnMobile: true }, CR_METRIC ]} - detailsLink={url.sitePath(site, `/custom-prop-values/${prop}`)} + detailsLink={url.sitePath(`custom-prop-values/${prop}`)} externalLinkDest={externalLinkDest()} maybeHideDetails={true} query={query} diff --git a/assets/js/dashboard/stats/behaviours/props.js b/assets/js/dashboard/stats/behaviours/props.js index 7eb7e32618fb..4986b215df77 100644 --- a/assets/js/dashboard/stats/behaviours/props.js +++ b/assets/js/dashboard/stats/behaviours/props.js @@ -98,7 +98,7 @@ export default function Properties(props) { BUILD_EXTRA && { name: 'total_revenue', label: 'Revenue', hiddenOnMobile: true }, BUILD_EXTRA && { name: 'average_revenue', label: 'Average', hiddenOnMobile: true } ]} - detailsLink={url.sitePath(site, `/custom-prop-values/${propKey}`)} + detailsLink={`/custom-prop-values/${propKey}`} maybeHideDetails={true} query={query} color="bg-red-50" diff --git a/assets/js/dashboard/stats/graph/stats-export.js b/assets/js/dashboard/stats/graph/stats-export.js index 6716b2eb1eb6..0c3613a81767 100644 --- a/assets/js/dashboard/stats/graph/stats-export.js +++ b/assets/js/dashboard/stats/graph/stats-export.js @@ -2,7 +2,7 @@ import React, { useState } from "react" import * as api from '../../api' import { getCurrentInterval } from "./interval-picker" -export default function StatsExport({site, query}) { +export default function StatsExport({ site, query }) { const [exporting, setExporting] = useState(false) function startExport() { @@ -50,4 +50,4 @@ export default function StatsExport({site, query}) { {!exporting && renderExportLink()} ) -} \ No newline at end of file +} diff --git a/assets/js/dashboard/stats/locations/index.js b/assets/js/dashboard/stats/locations/index.js index 488a6a398417..23f4a4bc2ded 100644 --- a/assets/js/dashboard/stats/locations/index.js +++ b/assets/js/dashboard/stats/locations/index.js @@ -4,108 +4,108 @@ import * as storage from '../../util/storage' import CountriesMap from './map' import * as api from '../../api' -import {apiPath, sitePath} from '../../util/url' +import { apiPath, sitePath } from '../../util/url' import ListReport from '../reports/list' import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics'; import { getFiltersByKeyPrefix } from '../../util/filters'; import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning'; -function Countries({query, site, onClick, afterFetchData}) { - function fetchData() { - return api.get(apiPath(site, '/countries'), query, { limit: 9 }) - } - - function renderIcon(country) { - return {country.flag} - } - - function getFilterFor(listItem) { - return { - prefix: "country", - filter: ["is", "country", [listItem['code']]], - labels: { [listItem['code']]: listItem['name'] } - } - } - - return ( - - ) +function Countries({ query, site, onClick, afterFetchData }) { + function fetchData() { + return api.get(apiPath(site, '/countries'), query, { limit: 9 }) + } + + function renderIcon(country) { + return {country.flag} + } + + function getFilterFor(listItem) { + return { + prefix: "country", + filter: ["is", "country", [listItem['code']]], + labels: { [listItem['code']]: listItem['name'] } + } + } + + return ( + + ) } -function Regions({query, site, onClick, afterFetchData}) { - function fetchData() { - return api.get(apiPath(site, '/regions'), query, {limit: 9}) - } - - function renderIcon(region) { - return {region.country_flag} - } - - function getFilterFor(listItem) { - return { - prefix: "region", - filter: ["is", "region", [listItem['code']]], - labels: { [listItem['code']]: listItem['name'] } - } - } - - return ( - - ) +function Regions({ query, site, onClick, afterFetchData }) { + function fetchData() { + return api.get(apiPath(site, '/regions'), query, { limit: 9 }) + } + + function renderIcon(region) { + return {region.country_flag} + } + + function getFilterFor(listItem) { + return { + prefix: "region", + filter: ["is", "region", [listItem['code']]], + labels: { [listItem['code']]: listItem['name'] } + } + } + + return ( + + ) } -function Cities({query, site, afterFetchData}) { - function fetchData() { - return api.get(apiPath(site, '/cities'), query, {limit: 9}) - } - - function renderIcon(city) { - return {city.country_flag} - } - - function getFilterFor(listItem) { - return { - prefix: "city", - filter: ["is", "city", [listItem['code']]], - labels: { [listItem['code']]: listItem['name'] } - } - } - - return ( - - ) +function Cities({ query, site, afterFetchData }) { + function fetchData() { + return api.get(apiPath(site, '/cities'), query, { limit: 9 }) + } + + function renderIcon(city) { + return {city.country_flag} + } + + function getFilterFor(listItem) { + return { + prefix: "city", + filter: ["is", "city", [listItem['code']]], + labels: { [listItem['code']]: listItem['name'] } + } + } + + return ( + + ) } @@ -117,116 +117,116 @@ const labelFor = { export default class Locations extends React.Component { constructor(props) { - super(props) - this.onCountryFilter = this.onCountryFilter.bind(this) - this.onRegionFilter = this.onRegionFilter.bind(this) - this.afterFetchData = this.afterFetchData.bind(this) - this.tabKey = `geoTab__${ props.site.domain}` - const storedTab = storage.getItem(this.tabKey) - this.state = { - mode: storedTab || 'map', - loading: true, - skipImportedReason: null - } - } - - componentDidUpdate(prevProps, prevState) { - const isRemovingFilter = (filterName) => { - return getFiltersByKeyPrefix(prevProps.query, filterName).length > 0 && - getFiltersByKeyPrefix(this.props.query, filterName).length == 0 - } - - if (this.state.mode === 'cities' && isRemovingFilter('region')) { - this.setMode('regions')() - } - - if (this.state.mode === 'regions' && isRemovingFilter('country')) { - this.setMode(this.countriesRestoreMode || 'countries')() - } - - if (this.props.query !== prevProps.query || this.state.mode !== prevState.mode) { - this.setState({loading: true}) - } - } - - setMode(mode) { - return () => { - storage.setItem(this.tabKey, mode) - this.setState({mode}) - } - } - - onCountryFilter(mode) { - return () => { - this.countriesRestoreMode = mode - this.setMode('regions')() - } - } - - onRegionFilter() { - this.setMode('cities')() - } - - afterFetchData(apiResponse) { - this.setState({loading: false, skipImportedReason: apiResponse.skip_imported_reason}) - } + super(props) + this.onCountryFilter = this.onCountryFilter.bind(this) + this.onRegionFilter = this.onRegionFilter.bind(this) + this.afterFetchData = this.afterFetchData.bind(this) + this.tabKey = `geoTab__${props.site.domain}` + const storedTab = storage.getItem(this.tabKey) + this.state = { + mode: storedTab || 'map', + loading: true, + skipImportedReason: null + } + } + + componentDidUpdate(prevProps, prevState) { + const isRemovingFilter = (filterName) => { + return getFiltersByKeyPrefix(prevProps.query, filterName).length > 0 && + getFiltersByKeyPrefix(this.props.query, filterName).length == 0 + } + + if (this.state.mode === 'cities' && isRemovingFilter('region')) { + this.setMode('regions')() + } + + if (this.state.mode === 'regions' && isRemovingFilter('country')) { + this.setMode(this.countriesRestoreMode || 'countries')() + } + + if (this.props.query !== prevProps.query || this.state.mode !== prevState.mode) { + this.setState({ loading: true }) + } + } + + setMode(mode) { + return () => { + storage.setItem(this.tabKey, mode) + this.setState({ mode }) + } + } + + onCountryFilter(mode) { + return () => { + this.countriesRestoreMode = mode + this.setMode('regions')() + } + } + + onRegionFilter() { + this.setMode('cities')() + } + + afterFetchData(apiResponse) { + this.setState({ loading: false, skipImportedReason: apiResponse.skip_imported_reason }) + } renderContent() { - switch(this.state.mode) { - case "cities": - return - case "regions": - return - case "countries": - return - case "map": - default: - return - } - } + switch (this.state.mode) { + case "cities": + return + case "regions": + return + case "countries": + return + case "map": + default: + return + } + } renderPill(name, mode) { - const isActive = this.state.mode === mode - - if (isActive) { - return ( - - ) - } - - return ( - - ) - } + const isActive = this.state.mode === mode + + if (isActive) { + return ( + + ) + } + + return ( + + ) + } render() { - return ( -
-
-
-

- {labelFor[this.state.mode] || 'Locations'} -

- -
-
- { this.renderPill('Map', 'map') } - { this.renderPill('Countries', 'countries') } - { this.renderPill('Regions', 'regions') } - { this.renderPill('Cities', 'cities') } -
-
- {this.renderContent()} -
- ) - } + return ( +
+
+
+

+ {labelFor[this.state.mode] || 'Locations'} +

+ +
+
+ {this.renderPill('Map', 'map')} + {this.renderPill('Countries', 'countries')} + {this.renderPill('Regions', 'regions')} + {this.renderPill('Cities', 'cities')} +
+
+ {this.renderContent()} +
+ ) + } } diff --git a/assets/js/dashboard/stats/locations/map.js b/assets/js/dashboard/stats/locations/map.js index 4006607c80ea..2cd09c8a340b 100644 --- a/assets/js/dashboard/stats/locations/map.js +++ b/assets/js/dashboard/stats/locations/map.js @@ -28,7 +28,7 @@ class Countries extends React.Component { componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { // eslint-disable-next-line react/no-did-update-set-state - this.setState({loading: true, countries: null}) + this.setState({ loading: true, countries: null }) this.fetchCountries().then(this.drawMap) } } @@ -49,19 +49,19 @@ class Countries extends React.Component { getDataset() { const dataset = {}; - var onlyValues = this.state.countries.map(function(obj){ return obj.visitors }); + var onlyValues = this.state.countries.map(function(obj) { return obj.visitors }); var maxValue = Math.max.apply(null, onlyValues); // eslint-disable-next-line no-undef const paletteScale = d3.scale.linear() - .domain([0,maxValue]) + .domain([0, maxValue]) .range([ this.state.darkTheme ? "#2e3954" : "#f3ebff", this.state.darkTheme ? "#6366f1" : "#a779e9" ]) - this.state.countries.forEach(function(item){ - dataset[item.alpha_3] = {numberOfThings: item.visitors, fillColor: paletteScale(item.visitors)}; + this.state.countries.forEach(function(item) { + dataset[item.alpha_3] = { numberOfThings: item.visitors, fillColor: paletteScale(item.visitors) }; }); return dataset @@ -69,18 +69,18 @@ class Countries extends React.Component { updateCountries() { this.fetchCountries().then(() => { - this.map.updateChoropleth(this.getDataset(), {reset: true}) + this.map.updateChoropleth(this.getDataset(), { reset: true }) }) } fetchCountries() { - return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, {limit: 300}) + return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, { limit: 300 }) .then((response) => { if (this.props.afterFetchData) { this.props.afterFetchData(response) } - this.setState({loading: false, countries: response.results}) + this.setState({ loading: false, countries: response.results }) }) } @@ -154,9 +154,9 @@ class Countries extends React.Component { if (this.state.countries) { return ( <> -
- - { this.geolocationDbNotice() } +
+ + {this.geolocationDbNotice()} ) } @@ -167,9 +167,9 @@ class Countries extends React.Component { render() { return ( - { this.state.loading &&
} + {this.state.loading &&
} - { this.renderBody() } + {this.renderBody()}
) diff --git a/assets/js/dashboard/stats/modals/conversions.js b/assets/js/dashboard/stats/modals/conversions.js index 1a245918cc2f..38efbfa7d032 100644 --- a/assets/js/dashboard/stats/modals/conversions.js +++ b/assets/js/dashboard/stats/modals/conversions.js @@ -69,7 +69,7 @@ function ConversionsModal(props) { {listItem.name} @@ -117,7 +117,7 @@ function ConversionsModal(props) { } return ( - + {renderBody()} {loading && renderLoading()} {!loading && moreResultsAvailable && renderLoadMore()} diff --git a/assets/js/dashboard/stats/modals/entry-pages.js b/assets/js/dashboard/stats/modals/entry-pages.js index 3c7004f42184..04007c6fd68c 100644 --- a/assets/js/dashboard/stats/modals/entry-pages.js +++ b/assets/js/dashboard/stats/modals/entry-pages.js @@ -81,7 +81,7 @@ class EntryPagesModal extends React.Component { + {this.renderBody()} {this.renderLoading()} diff --git a/assets/js/dashboard/stats/modals/exit-pages.js b/assets/js/dashboard/stats/modals/exit-pages.js index 8dc82bec08ff..25be36b3ac23 100644 --- a/assets/js/dashboard/stats/modals/exit-pages.js +++ b/assets/js/dashboard/stats/modals/exit-pages.js @@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom' import Modal from './modal' import * as api from '../../api' -import numberFormatter, {percentageFormatter} from '../../util/number-formatter' +import numberFormatter, { percentageFormatter } from '../../util/number-formatter' import { parseQuery } from '../../query' import { trimURL, updatedQuery } from '../../util/url' import { hasGoalFilter, replaceFilterByPrefix } from "../../util/filters"; @@ -62,7 +62,7 @@ class ExitPagesModal extends React.Component { + {this.renderBody()} {this.renderLoading()} diff --git a/assets/js/dashboard/stats/modals/filter-modal.js b/assets/js/dashboard/stats/modals/filter-modal.js index d81c08854bab..8d3ccd175861 100644 --- a/assets/js/dashboard/stats/modals/filter-modal.js +++ b/assets/js/dashboard/stats/modals/filter-modal.js @@ -2,9 +2,9 @@ import React from "react"; import { withRouter } from 'react-router-dom' import Modal from './modal' -import { EVENT_PROPS_PREFIX, FILTER_GROUP_TO_MODAL_TYPE, formatFilterGroup, FILTER_OPERATIONS, getFilterGroup, FILTER_MODAL_TO_FILTER_GROUP} from '../../util/filters' +import { EVENT_PROPS_PREFIX, FILTER_GROUP_TO_MODAL_TYPE, formatFilterGroup, FILTER_OPERATIONS, getFilterGroup, FILTER_MODAL_TO_FILTER_GROUP } from '../../util/filters' import { parseQuery } from '../../query' -import { siteBasePath, updatedQuery } from '../../util/url' +import { updatedQuery } from '../../util/url' import { shouldIgnoreKeypress } from '../../keybinding' import { cleanLabels } from "../../util/filters" import FilterModalGroup from "./filter-modal-group" @@ -83,7 +83,7 @@ class FilterModal extends React.Component { selectFiltersAndCloseModal(filters) { this.props.history.replace({ - pathname: siteBasePath(this.props.site), + pathname: '/', search: updatedQuery({ filters: filters, labels: cleanLabels(filters, this.state.labelState) @@ -125,7 +125,7 @@ class FilterModal extends React.Component { onDeleteRow(id) { this.setState(prevState => { - const filterState = {...prevState.filterState} + const filterState = { ...prevState.filterState } delete filterState[id] return { filterState } }) @@ -133,7 +133,7 @@ class FilterModal extends React.Component { render() { return ( - +

Filter by {formatFilterGroup(this.state.modalType)}

diff --git a/assets/js/dashboard/stats/modals/google-keywords.js b/assets/js/dashboard/stats/modals/google-keywords.js index 39ba7f3b8fca..4dfd45972679 100644 --- a/assets/js/dashboard/stats/modals/google-keywords.js +++ b/assets/js/dashboard/stats/modals/google-keywords.js @@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom' import Modal from './modal' import * as api from '../../api' import numberFormatter, { percentageFormatter } from '../../util/number-formatter' -import {parseQuery} from '../../query' +import { parseQuery } from '../../query' import RocketIcon from './rocket-icon' class GoogleKeywordsModal extends React.Component { @@ -17,7 +17,7 @@ class GoogleKeywordsModal extends React.Component { } componentDidMount() { - api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/Google`, this.state.query, {limit: 100}) + api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/Google`, this.state.query, { limit: 100 }) .then((res) => this.setState({ loading: false, searchTerms: res.search_terms, @@ -99,7 +99,7 @@ class GoogleKeywordsModal extends React.Component {
- { this.renderKeywords() } + {this.renderKeywords()}
) @@ -108,8 +108,8 @@ class GoogleKeywordsModal extends React.Component { render() { return ( - - { this.renderBody() } + + {this.renderBody()} ) } diff --git a/assets/js/dashboard/stats/modals/modal.js b/assets/js/dashboard/stats/modals/modal.js index 4b031f3600dc..a6cbbabded39 100644 --- a/assets/js/dashboard/stats/modals/modal.js +++ b/assets/js/dashboard/stats/modals/modal.js @@ -57,7 +57,7 @@ class Modal extends React.Component { } close() { - this.props.history.push(`/${encodeURIComponent(this.props.site.domain)}${this.props.location.search}`) + this.props.history.push(`/${this.props.location.search}`) } /** diff --git a/assets/js/dashboard/stats/modals/pages.js b/assets/js/dashboard/stats/modals/pages.js index 643938352fde..d564a2689c54 100644 --- a/assets/js/dashboard/stats/modals/pages.js +++ b/assets/js/dashboard/stats/modals/pages.js @@ -65,7 +65,7 @@ class PagesModal extends React.Component { -

Top Pages

+

Top Pages hi

@@ -141,7 +141,7 @@ class PagesModal extends React.Component { render() { return ( - + {this.renderBody()} {this.renderLoading()} diff --git a/assets/js/dashboard/stats/modals/props.js b/assets/js/dashboard/stats/modals/props.js index 0fcd1c04f562..64fe66a97b57 100644 --- a/assets/js/dashboard/stats/modals/props.js +++ b/assets/js/dashboard/stats/modals/props.js @@ -25,7 +25,8 @@ const Money = maybeRequire().default function PropsModal(props) { const site = props.site const query = parseQuery(props.location.search, site) - const propKey = props.location.pathname.split('/').pop() + + const propKey = props.location.pathname.split('/').filter(i => i).pop() const [loading, setLoading] = useState(true) const [moreResultsAvailable, setMoreResultsAvailable] = useState(false) @@ -71,7 +72,7 @@ function PropsModal(props) { {url.trimURL(listItem.name, 30)} @@ -125,7 +126,7 @@ function PropsModal(props) { } return ( - + {renderBody()} {loading && renderLoading()} {!loading && moreResultsAvailable && renderLoadMore()} diff --git a/assets/js/dashboard/stats/modals/referrer-drilldown.js b/assets/js/dashboard/stats/modals/referrer-drilldown.js index 49d33c547690..ff1e4caf5bf2 100644 --- a/assets/js/dashboard/stats/modals/referrer-drilldown.js +++ b/assets/js/dashboard/stats/modals/referrer-drilldown.js @@ -3,8 +3,8 @@ import { Link, withRouter } from 'react-router-dom' import Modal from './modal' import * as api from '../../api' -import numberFormatter, {durationFormatter} from '../../util/number-formatter' -import {parseQuery} from '../../query' +import numberFormatter, { durationFormatter } from '../../util/number-formatter' +import { parseQuery } from '../../query' import { updatedQuery } from "../../util/url"; import { hasGoalFilter, replaceFilterByPrefix } from "../../util/filters"; @@ -20,8 +20,8 @@ class ReferrerDrilldownModal extends React.Component { componentDidMount() { const detailed = this.showExtra() - api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${this.props.match.params.referrer}`, this.state.query, {limit: 100, detailed}) - .then((response) => this.setState({loading: false, referrers: response.results})) + api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${this.props.match.params.referrer}`, this.state.query, { limit: 100, detailed }) + .then((response) => this.setState({ loading: false, referrers: response.results })) } showExtra() { @@ -45,7 +45,7 @@ class ReferrerDrilldownModal extends React.Component { } formatBounceRate(ref) { - if (typeof(ref.bounce_rate) === 'number') { + if (typeof (ref.bounce_rate) === 'number') { return ref.bounce_rate + '%' } else { return '-' @@ -53,7 +53,7 @@ class ReferrerDrilldownModal extends React.Component { } formatDuration(referrer) { - if (typeof(referrer.visit_duration) === 'number') { + if (typeof (referrer.visit_duration) === 'number') { return durationFormatter(referrer.visit_duration) } else { return '-' @@ -77,12 +77,12 @@ class ReferrerDrilldownModal extends React.Component { {referrer.name} - { this.renderExternalLink(referrer.name) } + {this.renderExternalLink(referrer.name)} ) } @@ -91,13 +91,13 @@ class ReferrerDrilldownModal extends React.Component { return ( - { this.renderReferrerName(referrer) } + {this.renderReferrerName(referrer)} - {this.showConversionRate() && {numberFormatter(referrer.total_visitors)} } + {this.showConversionRate() && {numberFormatter(referrer.total_visitors)}} {numberFormatter(referrer.visitors)} - {this.showExtra() && {this.formatBounceRate(referrer)} } - {this.showExtra() && {this.formatDuration(referrer)} } - {this.showConversionRate() && {referrer.conversion_rate}% } + {this.showExtra() && {this.formatBounceRate(referrer)}} + {this.showExtra() && {this.formatDuration(referrer)}} + {this.showConversionRate() && {referrer.conversion_rate}%} ) } @@ -126,7 +126,7 @@ class ReferrerDrilldownModal extends React.Component { - { this.state.referrers.map(this.renderReferrer.bind(this)) } + {this.state.referrers.map(this.renderReferrer.bind(this))}
@@ -137,8 +137,8 @@ class ReferrerDrilldownModal extends React.Component { render() { return ( - - { this.renderBody() } + + {this.renderBody()} ) } diff --git a/assets/js/dashboard/stats/modals/sources.js b/assets/js/dashboard/stats/modals/sources.js index 2f272f19e017..8e9efa068dc9 100644 --- a/assets/js/dashboard/stats/modals/sources.js +++ b/assets/js/dashboard/stats/modals/sources.js @@ -152,7 +152,7 @@ class SourcesModal extends React.Component { render() { return ( - +

{this.title()}

diff --git a/assets/js/dashboard/stats/modals/table.js b/assets/js/dashboard/stats/modals/table.js index 20eea170c3bd..fc4861776a10 100644 --- a/assets/js/dashboard/stats/modals/table.js +++ b/assets/js/dashboard/stats/modals/table.js @@ -4,7 +4,7 @@ import { Link, withRouter } from 'react-router-dom' import Modal from './modal' import * as api from '../../api' import numberFormatter from '../../util/number-formatter' -import {parseQuery} from '../../query' +import { parseQuery } from '../../query' import { cleanLabels, hasGoalFilter, replaceFilterByPrefix } from "../../util/filters"; import { updatedQuery } from "../../util/url"; @@ -18,8 +18,8 @@ class ModalTable extends React.Component { } componentDidMount() { - api.get(this.props.endpoint, this.state.query, {limit: 100}) - .then((response) => this.setState({loading: false, list: response.results})) + api.get(this.props.endpoint, this.state.query, { limit: 100 }) + .then((response) => this.setState({ loading: false, list: response.results })) } showConversionRate() { @@ -56,7 +56,7 @@ class ModalTable extends React.Component { className="hover:underline" to={{ search: updatedQuery({ filters, labels }), - pathname: `/${encodeURIComponent(this.props.site.domain)}` + pathname: `/` }} > {this.props.renderIcon && this.props.renderIcon(tableItem)} @@ -97,7 +97,7 @@ class ModalTable extends React.Component { - { this.state.list.map(this.renderTableItem.bind(this)) } + {this.state.list.map(this.renderTableItem.bind(this))} @@ -110,8 +110,8 @@ class ModalTable extends React.Component { render() { return ( - - { this.renderBody() } + + {this.renderBody()} ) } diff --git a/assets/js/dashboard/stats/more-link.js b/assets/js/dashboard/stats/more-link.js index 95dfdf284e8b..3940cbbc5050 100644 --- a/assets/js/dashboard/stats/more-link.js +++ b/assets/js/dashboard/stats/more-link.js @@ -5,7 +5,7 @@ function detailsIcon() { return ( 0) { return (
- { detailsIcon() } + {detailsIcon()} DETAILS
diff --git a/assets/js/dashboard/stats/pages/index.js b/assets/js/dashboard/stats/pages/index.js index 991605fd4e09..ee76a8bdced1 100644 --- a/assets/js/dashboard/stats/pages/index.js +++ b/assets/js/dashboard/stats/pages/index.js @@ -30,7 +30,7 @@ function EntryPages({ query, site, afterFetchData }) { getFilterFor={getFilterFor} keyLabel="Entry page" metrics={maybeWithCR([{ ...VISITORS_METRIC, label: 'Unique Entrances' }], query)} - detailsLink={url.sitePath(site, '/entry-pages')} + detailsLink={url.sitePath('entry-pages')} query={query} externalLinkDest={externalLinkDest} color="bg-orange-50" @@ -61,7 +61,7 @@ function ExitPages({ query, site, afterFetchData }) { getFilterFor={getFilterFor} keyLabel="Exit page" metrics={maybeWithCR([{ ...VISITORS_METRIC, label: "Unique Exits" }], query)} - detailsLink={url.sitePath(site, '/exit-pages')} + detailsLink={url.sitePath('exit-pages')} query={query} externalLinkDest={externalLinkDest} color="bg-orange-50" @@ -92,7 +92,7 @@ function TopPages({ query, site, afterFetchData }) { getFilterFor={getFilterFor} keyLabel="Page" metrics={maybeWithCR([VISITORS_METRIC], query)} - detailsLink={url.sitePath(site, '/pages')} + detailsLink={url.sitePath('pages')} query={query} externalLinkDest={externalLinkDest} color="bg-orange-50" @@ -107,7 +107,7 @@ const labelFor = { } export default function Pages(props) { - const {site, query} = props + const { site, query } = props const tabKey = `pageTab__${site.domain}` const storedTab = storage.getItem(tabKey) const [mode, setMode] = useState(storedTab || 'pages') diff --git a/assets/js/dashboard/stats/sources/referrer-list.js b/assets/js/dashboard/stats/sources/referrer-list.js index f0069e8578df..3ebc1af6381d 100644 --- a/assets/js/dashboard/stats/sources/referrer-list.js +++ b/assets/js/dashboard/stats/sources/referrer-list.js @@ -5,14 +5,14 @@ import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics' import ListReport from '../reports/list' import ImportedQueryUnsupportedWarning from '../../stats/imported-query-unsupported-warning' -export default function Referrers({source, site, query}) { +export default function Referrers({ source, site, query }) { const [skipImportedReason, setSkipImportedReason] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => setLoading(true), [query]) function fetchReferrers() { - return api.get(url.apiPath(site, `/referrers/${encodeURIComponent(source)}`), query, {limit: 9}) + return api.get(url.apiPath(site, `/referrers/${encodeURIComponent(source)}`), query, { limit: 9 }) } function afterFetchReferrers(apiResponse) { @@ -48,7 +48,7 @@ export default function Referrers({source, site, query}) {

Top Referrers

- +
- { - this.setState({ loading: false, searchTerms: [], notConfigured: true, error: true, isAdmin: error.payload.is_admin }) - } + })).catch((error) => { + this.setState({ loading: false, searchTerms: [], notConfigured: true, error: true, isAdmin: error.payload.is_admin }) + } ) } @@ -59,7 +58,7 @@ export default class SearchTerms extends React.Component { > - { term.name } + {term.name} @@ -69,7 +68,7 @@ export default class SearchTerms extends React.Component { } renderList() { - if (this.state.unsupportedFilters) { + if (this.state.unsupportedFilters) { return (
@@ -81,10 +80,10 @@ export default class SearchTerms extends React.Component {
- This site is not connected to Search Console so we cannot show the search terms - {this.state.isAdmin && this.state.error && <>

Please click below to connect your Search Console account.

} + This site is not connected to Search Console so we cannot show the search terms + {this.state.isAdmin && this.state.error && <>

Please click below to connect your Search Console account.

}
- {this.state.isAdmin && Connect with Google } + {this.state.isAdmin && Connect with Google}
) } else if (this.state.searchTerms.length > 0) { @@ -114,8 +113,8 @@ export default class SearchTerms extends React.Component { return (

Search Terms

- { this.renderList() } - + {this.renderList()} +
) } @@ -124,10 +123,10 @@ export default class SearchTerms extends React.Component { render() { return (
- { this.state.loading &&
} + {this.state.loading &&
} - { this.renderContent() } + {this.renderContent()}
diff --git a/assets/js/dashboard/stats/sources/source-list.js b/assets/js/dashboard/stats/sources/source-list.js index 32c6a790beec..806dfc8195a3 100644 --- a/assets/js/dashboard/stats/sources/source-list.js +++ b/assets/js/dashboard/stats/sources/source-list.js @@ -48,7 +48,7 @@ function AllSources(props) { getFilterFor={getFilterFor} keyLabel="Source" metrics={maybeWithCR([VISITORS_METRIC], query)} - detailsLink={url.sitePath(site, '/sources')} + detailsLink={url.sitePath('sources')} renderIcon={renderIcon} query={query} color="bg-blue-50" @@ -78,7 +78,7 @@ function UTMSources(props) { getFilterFor={getFilterFor} keyLabel={utmTag.label} metrics={maybeWithCR([VISITORS_METRIC], query)} - detailsLink={url.sitePath(site, utmTag.endpoint)} + detailsLink={url.sitePath(utmTag.endpoint)} query={query} color="bg-blue-50" /> @@ -178,7 +178,7 @@ export default function SourceList(props) {

Top Sources

- +
{renderTabs()}
diff --git a/assets/js/dashboard/util/url.js b/assets/js/dashboard/util/url.js index e9349e0d343d..609f11612374 100644 --- a/assets/js/dashboard/util/url.js +++ b/assets/js/dashboard/util/url.js @@ -1,15 +1,11 @@ import JsonURL from '@jsonurl/jsonurl' export function apiPath(site, path = '') { - return `/api/stats/${encodeURIComponent(site.domain)}${path}` + return `/api/stats/${encodeURIComponent(site.domain)}${path}/` } -export function siteBasePath(site, path = '') { - return `/${encodeURIComponent(site.domain)}${path}` -} - -export function sitePath(site, path = '') { - return siteBasePath(site, path) + window.location.search +export function sitePath(path = '') { + return `/${path}` + window.location.search } export function setQuery(key, value) { diff --git a/config/config.exs b/config/config.exs index 8d248d091822..7a610cfb2795 100644 --- a/config/config.exs +++ b/config/config.exs @@ -38,7 +38,7 @@ config :tailwind, config :ua_inspector, database_path: "priv/ua_inspector", - remote_release: "6.3.0" + remote_release: "6.3.2" config :ref_inspector, database_path: "priv/ref_inspector" diff --git a/lib/plausible/verification/checks/snippet.ex b/lib/plausible/verification/checks/snippet.ex index 9375a9112239..c1fbcfc2e043 100644 --- a/lib/plausible/verification/checks/snippet.ex +++ b/lib/plausible/verification/checks/snippet.ex @@ -11,8 +11,8 @@ defmodule Plausible.Verification.Checks.Snippet do @impl true def perform(%State{assigns: %{document: document}} = state) do - in_head = Floki.find(document, "head script[data-domain]") - in_body = Floki.find(document, "body script[data-domain]") + in_head = Floki.find(document, "head script[data-domain][src]") + in_body = Floki.find(document, "body script[data-domain][src]") all = in_head ++ in_body diff --git a/mix.lock b/mix.lock index 04921d89ac44..9474037a6057 100644 --- a/mix.lock +++ b/mix.lock @@ -81,7 +81,7 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, "mjml": {:hex, :mjml, "1.5.0", "20a4ed2490a60c6928d45a69b64fb45ce8d8bdac686ef689315b0adda69c6406", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "44dc36c0fccf52eeb8e0afcb26a863ba41a5f9adcb71bb32e084619a13bb4cdf"}, "mjml_eex": {:hex, :mjml_eex, "0.11.0", "f0845730f4caccddea7c98ab5ad1485831446b7c09896fa5ed54b3fa0c431e72", [:mix], [{:erlexec, "~> 2.0", [hex: :erlexec, repo: "hexpm", optional: true]}, {:mjml, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :mjml, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0c60732fe766336ec504a94cad4ebf30405f05fa8920a544ff0ef936252438ac"}, @@ -146,7 +146,7 @@ "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "tls_certificate_check": {:hex, :tls_certificate_check, "1.21.0", "042ab2c0c860652bc5cf69c94e3a31f96676d14682e22ec7813bd173ceff1788", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6cee6cffc35a390840d48d463541d50746a7b0e421acaadb833cfc7961e490e7"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, - "ua_inspector": {:hex, :ua_inspector, "3.9.0", "2021bbddb1ee41f202da7f006fb09f5c5617ad579108b7b9bcf1881828462f04", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0f2d1b9806e78b14b91b6f20eaaa4a68f8989f4f2a99a376b874c295ac50a30d"}, + "ua_inspector": {:hex, :ua_inspector, "3.10.0", "eb2a889039c82855c4638227bae8434e37fe16a7430eb3dab323a80b4372f41d", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "ec619c3742bd26b27934c9f89448ea23cc252717b8fd3630ec1e24fac18a17e4"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, diff --git a/priv/ua_inspector/bot.bots.yml b/priv/ua_inspector/bot.bots.yml index 89632f58346f..f5ef176a3bfd 100644 --- a/priv/ua_inspector/bot.bots.yml +++ b/priv/ua_inspector/bot.bots.yml @@ -5,6 +5,11 @@ # @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later ############### +- regex: 'WireReaderBot(?:/([\d+.]+))?' + name: 'WireReaderBot' + category: 'Feed Fetcher' + url: 'https://wirereader.app/' + - regex: 'monitoring360bot' name: '360 Monitoring' category: 'Site Monitor' @@ -546,10 +551,10 @@ name: 'SEOmoz, Inc.' url: 'http://moz.com/' -- regex: 'facebookexternalhit|facebookplatform|facebookexternalua|facebookcatalog' - name: 'Facebook External Hit' +- regex: 'facebook(?:catalog|externalhit|externalua|platform|scraper)' + name: 'Facebook Crawler' category: 'Social Media Agent' - url: 'https://www.facebook.com/externalhit_uatext.php' + url: 'https://developers.facebook.com/docs/sharing/webmasters/crawler/' producer: name: 'Meta Platforms, Inc.' url: 'https://www.meta.com/' @@ -768,7 +773,15 @@ name: 'Visual Meta' url: 'https://www.shopalike.cz/' -- regex: 'Adwords-(?:DisplayAds|Express|Instant)|Google Web Preview|Google[ -]Publisher[ -]Plugin|Google-(?:Ads-Conversions|Ads-Qualify|Adwords|AMPHTML|Assess|Extended|HotelAdsVerifier|InspectionTool|PageRenderer|Read-Aloud|Safety|Shopping-Quality|Site-Verification|speakr|Stale-Content-Probe|Test|Youtube-Links)|(?:AdsBot|APIs|DuplexWeb|Feedfetcher|Mediapartners)-Google(?:-Mobile)?|Google(?:AdSenseInfeed|AssociationService|bot|Other|Prober|Producer)|Google.*/\+/web/snippet' +- regex: 'Googlebot-News' + name: 'Googlebot News' + category: 'Search bot' + url: 'https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers' + producer: + name: 'Google Inc.' + url: 'https://www.google.com/' + +- regex: 'Adwords-(?:DisplayAds|Express|Instant)|Google Web Preview|Google[ -]Publisher[ -]Plugin|Google-(?:adstxt|Ads-Conversions|Ads-Qualify|Adwords|AMPHTML|Assess|Extended|HotelAdsVerifier|InspectionTool|Lens|PageRenderer|Read-Aloud|Safety|Shopping-Quality|Site-Verification|Sites-Thumbnails|speakr|Stale-Content-Probe|Test|Youtube-Links)|(?:AdsBot|APIs|DuplexWeb|Feedfetcher|Mediapartners)-Google(?:-Mobile)?|Google(?:AdSenseInfeed|AssociationService|bot|Other|Prober|Producer|Sites)|Google.*/\+/web/snippet' name: 'Googlebot' category: 'Search bot' url: 'https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers' @@ -784,6 +797,14 @@ name: 'Google Inc.' url: 'https://www.google.com/' +- regex: 'Google-Area120-PrivacyPolicyFetcher' + name: 'Google Area 120 Privacy Policy Fetcher' + category: 'Crawler' + url: 'https://area120.google.com/' + producer: + name: 'Google Inc.' + url: 'https://www.google.com/' + - regex: 'heritrix' name: 'Heritrix' category: 'Crawler' @@ -940,7 +961,7 @@ name: 'BIZON, OOO' url: 'https://bi.zone/' -- regex: 'masscan' +- regex: '.*masscan' name: 'masscan' url: 'https://github.com/robertdavidgraham/masscan' category: 'Crawler' @@ -1314,12 +1335,36 @@ url: '' - regex: 'SemrushBot' - name: 'Semrush Bot' + name: 'SemrushBot' category: 'Crawler' - url: 'http://www.semrush.com/bot.html' + url: 'https://www.semrush.com/bot/' producer: - name: 'SEMrush' - url: 'http://www.semrush.com' + name: 'Semrush Inc.' + url: 'https://www.semrush.com/' + +- regex: 'SerpReputationManagementAgent/[\d.]+' + name: 'Semrush Reputation Management' + category: 'Service Agent' + url: 'https://www.semrush.com/bot/' + producer: + name: 'Semrush Inc.' + url: 'https://www.semrush.com/' + +- regex: 'SplitSignalBot' + name: 'SplitSignalBot' + category: 'Crawler' + url: 'https://www.semrush.com/bot/' + producer: + name: 'Semrush Inc.' + url: 'https://www.semrush.com/' + +- regex: 'SiteAuditBot/[\d.]+' + name: 'SiteAuditBot' + category: 'Crawler' + url: 'https://www.semrush.com/bot/' + producer: + name: 'Semrush Inc.' + url: 'https://www.semrush.com/' - regex: 'SensikaBot' name: 'Sensika Bot' @@ -1565,7 +1610,7 @@ category: 'Feed Reader' url: 'https://theoldreader.com' -- regex: 'Trackable/0.1' +- regex: 'Trackable/0\.1' name: 'Chartable' category: 'Site Monitor' url: 'https://help.chartable.com/article/34-what-is-the-trackable-analytics-prefix' @@ -1589,13 +1634,13 @@ name: 'iParadigms, LLC.' url: 'http://www.turnitin.com' -- regex: 'TweetedTimes Bot' +- regex: 'TweetedTimes' name: 'TweetedTimes Bot' category: 'Crawler' - url: 'http://tweetedtimes.com' + url: 'https://tweetedtimes.com/' producer: name: 'TweetedTimes' - url: 'http://tweetedtimes.com/' + url: 'https://tweetedtimes.com/' - regex: 'TweetmemeBot' name: 'Tweetmeme Bot' @@ -1636,21 +1681,21 @@ name: 'UkrNet Ltd' url: 'https://www.ukr.net/' -- regex: 'Uptimebot' +- regex: 'Uptime(?:bot)?/[\d.]+' name: 'Uptimebot' category: 'Site Monitor' - url: 'https://uptime.com/uptimebot' + url: 'https://uptime.com/uptime-bot' producer: name: 'Uptime' - url: 'https://uptime.com' + url: 'https://uptime.com/' - regex: 'UptimeRobot' - name: 'Uptime Robot' + name: 'UptimeRobot' category: 'Site Monitor' - url: '' + url: 'https://uptimerobot.com/' producer: name: 'Uptime Robot' - url: 'http://uptimerobot.com' + url: 'https://uptimerobot.com/' - regex: 'URLAppendBot' name: 'URLAppendBot' @@ -1880,7 +1925,23 @@ name: 'Yahoo! Japan Corp.' url: 'https://www.yahoo.co.jp/' -- regex: 'Yandex(?:(?:\.Gazeta |Accessibility|Mobile|MobileScreenShot|RenderResources|Screenshot|Sprav)?Bot|(?:AdNet|Antivirus|Blogs|Calendar|Catalog|Direct|Favicons|ForDomain|ImageResizer|Images|Market|Media|Metrika|News|OntoDB(?:API)?|Pagechecker|Partner|RCA|SearchShop|(?:News|Site)links|Tracker|Turbo|Verticals|Vertis|Video|Webmaster))|YaDirectFetcher' +- regex: 'Y!J-ASR' + name: 'Yahoo! Japan ASR' + category: 'Crawler' + url: 'https://support.yahoo-net.jp/PccSearch/s/article/H000007955' + producer: + name: 'Yahoo! Japan Corp.' + url: 'https://www.yahoo.co.jp/' + +- regex: '^Y!J' + name: 'Yahoo! Japan' + category: 'Crawler' + url: 'https://support.yahoo-net.jp/PccSearch/s/article/H000007955' + producer: + name: 'Yahoo! Japan Corp.' + url: 'https://www.yahoo.co.jp/' + +- regex: 'Yandex(?:(?:\.Gazeta |Accessibility|Mobile|MobileScreenShot|RenderResources|Screenshot|Sprav)?Bot|(?:AdNet|Antivirus|Blogs|Calendar|Catalog|Direct|Favicons|ForDomain|ImageResizer|Images|Market|Media|Metrika|News|OntoDB(?:API)?|Pagechecker|Partner|RCA|SearchShop|(?:News|Site)links|Tracker|Turbo|Userproxy|Verticals|Vertis|Video|Webmaster))|YaDirectFetcher' name: 'Yandex Bot' category: 'Search bot' url: 'https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html' @@ -1978,7 +2039,7 @@ name: 'Pinterest' url: 'https://www.pinterest.com/' -- regex: 'Site24x7' +- regex: '.*Site24x7' name: 'Site24x7 Website Monitoring' category: 'Site Monitor' url: 'https://www.site24x7.com/site24x7-faq.html' @@ -1986,6 +2047,14 @@ name: 'Site24x7' url: 'https://www.site24x7.com' +- regex: '.* HLB/[\d.]+' + name: 'Site24x7 Defacement Monitor' + category: 'Site Monitor' + url: 'https://support.site24x7.com/portal/en/kb/articles/default-user-agent-used-in-website-defacement-monitor' + producer: + name: 'Site24x7' + url: 'https://www.site24x7.com/' + - regex: 's~snapchat-proxy' name: 'Snapchat Proxy' category: 'Crawler' @@ -2172,14 +2241,6 @@ name: 'Siteimprove GmbH' url: 'https://siteimprove.com/' -- regex: 'Image size by Siteimprove\.com' - name: 'Siteimprove' - category: 'Search bot' - url: 'https://siteimprove.com/' - producer: - name: 'Siteimprove GmbH' - url: 'https://siteimprove.com/' - - regex: 'CATExplorador' name: 'CATExplorador' category: 'Search bot' @@ -2394,7 +2455,7 @@ name: 'PPC Labs LLC' url: 'https://www.adbeat.com/' -- regex: 'BW/[\d.]+' +- regex: '(?:BuiltWith|BW)/[\d.]+' name: 'BuiltWith' category: 'Crawler' url: 'https://builtwith.com/biup' @@ -2544,8 +2605,16 @@ url: 'https://github.com/projectdiscovery/httpx' category: 'Crawler' producer: - name: '' - url: '' + name: 'ProjectDiscovery, Inc.' + url: 'https://projectdiscovery.io/' + +- regex: '.*\.oast\.' + name: 'Interactsh' + category: 'Security Checker' + url: 'https://github.com/projectdiscovery/interactsh' + producer: + name: 'ProjectDiscovery, Inc.' + url: 'https://projectdiscovery.io/' - regex: 'scaninfo@(?:expanseinc|paloaltonetworks)\.com' name: 'Expanse' @@ -3156,14 +3225,6 @@ name: 'Marc Huemer' url: 'https://www.netzzappen.com/' -- regex: 'SerpReputationManagementAgent/[\d.]+' - name: 'SEMrush Reputation Management' - category: 'Service Agent' - url: 'https://www.semrush.com/bot/' - producer: - name: 'SEMrush' - url: 'https://www.semrush.com/' - - regex: 'panscient\.com' name: 'Panscient' category: 'Crawler' @@ -3220,7 +3281,7 @@ name: 'New Work SE' url: 'https://www.xing.com/' -- regex: 'RepoLookoutBot/[\d.]+' +- regex: 'RepoLookoutBot/v?[\d.]+' name: 'Repo Lookout' category: 'Security Checker' url: 'https://www.repo-lookout.org/' @@ -3492,14 +3553,6 @@ name: 'Lumar' url: 'https://www.lumar.io/' -- regex: 'RepoLookoutBot' - name: 'Repo Lookout' - category: 'Crawler' - url: 'https://www.repo-lookout.org/' - producer: - name: 'Crissy Field GmbH' - url: 'https://www.crissyfield.de/' - - regex: 'researchscan\.comsys\.rwth-aachen\.de' name: 'Research Scan' category: 'Crawler' @@ -3654,6 +3707,11 @@ category: 'Crawler' url: 'https://geedo.com/bot/' +- regex: 'GeedoProductSearch' + name: 'GeedoProductSearch' + category: 'Crawler' + url: 'https://geedo.com/product-search/' + - regex: 'BackupLand(?:/([\d+.]+))?' name: 'BackupLand' category: 'Crawler' @@ -3754,6 +3812,16 @@ category: 'Crawler' url: 'https://github.com/aaamoon/copilot-gpt4-service' +- regex: '^pdrl\.fm' + name: 'Podroll Analyzer' + category: 'Crawler' + url: 'https://podroll.fm' + +- regex: 'PodUptime/' + name: 'PodUptime' + category: 'Site Monitor' + url: 'https://poduptime.com' + - regex: 'anthropic-ai' name: 'Anthropic AI' category: 'Crawler' @@ -3994,9 +4062,436 @@ name: 'EMDASH SAS' url: 'https://www.fontradar.com/' -# Generic detections -- regex: 'nuhk|grub-client|Download Demon|SearchExpress|Microsoft URL Control|borg|altavista|dataminr\.com|tweetedtimes\.com|teoma|oegp|http%20client|htdig|mogimogi|larbin|scrubby|searchsight|semanticdiscovery|snappy|vortex(?!(?: Build|Plus))|zeal(?!ot)|dataparksearch|findlinks|BrowserMob|URL2PNG|ZooShot|GomezA|Google SketchUp|Read%20Later|7Siters|centuryb\.o\.t9|InterNaetBoten|EasyBib AutoCite|Bidtellect|tomnomnom/meg|cortex|Re-re Studio|adreview|AHC/|NameOfAgent|Request-Promise|ALittle Client|Hello,? world|wp_is_mobile|0xAbyssalDoesntExist|Anarchy99|daumoa,damoa,daum,daumos,duamoa,duam,duamos|^revolt|nvd0rz|xfa1|Hakai|gbrmss|fuck-your-hp|IDBTE4M CODE87|Antoine|Insomania|Hells-Net|b3astmode|Linux Gnu \(cow\)|Test Certificate Info|iplabel|Magellan|TheSafex?Internetx?Search|kirkland-signature|^xenu|^ZmEu|^(?:chrome|firefox|Zeus)$' +- regex: 'ViberUrlDownloader' + name: 'Viber Url Downloader' + category: 'Service Agent' + url: 'https://www.viber.com/' + producer: + name: 'Viber Media S.à r.l.' + url: 'https://www.viber.com/' + +- regex: '^Zeno$' + name: 'Zeno' + category: 'Crawler' + url: 'https://github.com/internetarchive/Zeno' + producer: + name: 'The Internet Archive' + url: 'https://archive.org/' + +- regex: 'Barracuda Sentinel' + name: 'Barracuda Sentinel' + category: 'Service Agent' + url: 'https://sentinel.barracudanetworks.com/' + producer: + name: 'Barracuda Networks, Inc.' + url: 'https://www.barracudanetworks.com/' + +- regex: 'RuxitSynthetic/[\d.]+' + name: 'RuxitSynthetic' + category: 'Site Monitor' + url: 'https://community.dynatrace.com/t5/Troubleshooting/Basic-Commands-for-Synthetic/ta-p/198164' + producer: + name: 'Dynatrace LLC' + url: 'https://www.dynatrace.com/' + +- regex: 'DynatraceSynthetic/[\d.]+' + name: 'DynatraceSynthetic' + category: 'Site Monitor' + url: 'https://community.dynatrace.com/t5/Troubleshooting/Basic-Commands-for-Synthetic/ta-p/198164' + producer: + name: 'Dynatrace LLC' + url: 'https://www.dynatrace.com/' + +- regex: 'sitebulb' + name: 'Sitebulb' + category: 'Crawler' + url: 'https://sitebulb.com/' + producer: + name: 'Sitebulb Limited' + url: 'https://sitebulb.com/' + +- regex: 'Monsidobot/[\d.]+' + name: 'Monsidobot' + category: 'Crawler' + url: 'https://monsido.com/bot-html' + producer: + name: 'Monsido LLC' + url: 'https://monsido.com/' + +- regex: 'AccompanyBot' + name: 'AccompanyBot' + category: 'Crawler' + url: 'https://www.accompany.com/' + producer: + name: 'Accompani, Inc' + url: 'https://www.accompany.com/' + +- regex: 'Ghost Inspector' + name: 'Ghost Inspector' + category: 'Site Monitor' + url: 'https://docs.ghostinspector.com/faq/#how-do-i-detect-ghost-inspector-test-runner-traffic-on-my-site' + producer: + name: 'Ghost Inspector, Inc.' + url: 'https://www.ghostinspector.com/' + +- regex: 'Cypress/[\d.]+' + name: 'Cypress' + category: 'Site Monitor' + url: 'https://github.com/cypress-io/cypress' + producer: + name: 'Cypress.io, Inc.' + url: 'https://www.cypress.io/' + +- regex: 'Google-Apps-Script' + name: 'Google Apps Script' + category: 'Service Agent' + url: 'https://www.google.com/script/start/' + +- regex: 'SiteOne-Crawler/[\d.]+' + name: 'SiteOne Crawler' + category: 'Crawler' + url: 'https://crawler.siteone.io/bot/' + producer: + name: 'SiteOne s.r.o.' + url: 'https://www.siteone.io/' + +- regex: 'Detectify' + name: 'Detectify' + category: 'Security Checker' + url: 'https://support.detectify.com/support/solutions/articles/48001049001-how-to-allow-detectify-to-access-your-site' + producer: + name: 'Detectify AB' + url: 'https://detectify.com/' + +- regex: 'DomCopBot' + name: 'DomCop Bot' + category: 'Crawler' + url: 'https://www.domcop.com/bot' + producer: + name: 'Axeman Technology Solutions LLP' + url: 'https://axemantech.com/' + +- regex: 'Paqlebot/[\d.]+' + name: 'Paqlebot' + category: 'Crawler' + url: 'https://www.paqle.dk/about/paqlebot' + producer: + name: 'Paqle A/S' + url: 'https://www.paqle.dk/' + +- regex: 'Wibybot' + name: 'Wibybot' + category: 'Crawler' + url: 'https://www.wiby.me/' + +- regex: 'Synapse' + name: 'Synapse' + category: 'Crawler' + url: 'https://github.com/matrix-org/synapse' + +- regex: 'OSZKbot/[\d.]+' + name: 'OSZKbot' + category: 'Crawler' + url: 'http://mekosztaly.oszk.hu/mia/' + producer: + name: 'National Szechenyi Library' + url: 'https://webarchivum.oszk.hu/' + +- regex: 'ZoomBot' + name: 'ZoomBot' + category: 'Crawler' + url: 'https://suite.seozoom.it/bot.html' + producer: + name: 'SEO Cube S.r.l.' + url: 'https://www.seocube.it/' + +- regex: 'RavenCrawler/[\d.]+' + name: 'RavenCrawler' + category: 'Crawler' + url: 'https://raventools.com/site-auditor/' + producer: + name: 'TapClicks, Inc.' + url: 'https://www.tapclicks.com/' + +- regex: 'KadoBot' + name: 'KadoBot' + category: 'Crawler' + url: 'https://www.kadolijst.nl/bot' + producer: + name: 'Kadolijst' + url: 'https://www.kadolijst.nl/' + +- regex: 'Dubbotbot/[\d.]+' + name: 'Dubbotbot' + category: 'Crawler' + url: 'https://help.dubbot.com/en/articles/6746594-example-custom-user-agent' + producer: + name: 'DubBot' + url: 'https://dubbot.com/' + +- regex: 'Swiftbot/[\d.]+' + name: 'Swiftbot' + category: 'Crawler' + url: 'https://swiftype.com/swiftbot' + producer: + name: 'Elasticsearch, B.V.' + url: 'https://www.elastic.co/' + +- regex: 'EyeMonIT' + name: 'EyeMonit' + category: 'Site Monitor' + url: 'https://eyemonit.com/' + producer: + name: 'EyeMonit' + url: 'https://eyemonit.com/' + +- regex: 'ThousandEyes' + name: 'ThousandEyes' + category: 'Site Monitor' + url: 'https://www.thousandeyes.com/' + producer: + name: 'Cisco Systems, Inc.' + url: 'https://www.cisco.com/' + +- regex: 'OmtrBot/[\d.]+' + name: 'OmtrBot' + category: 'Site Monitor' + +- regex: 'WebMon/[\d.]+' + name: 'WebMon' + category: 'Site Monitor' + +- regex: 'AdsTxtCrawlerTP/[\d.]+' + name: 'AdsTxtCrawlerTP' + category: 'Crawler' + +- regex: 'fragFINN' + name: 'fragFINN' + category: 'Crawler' + url: 'https://www.fragfinn.de/' + producer: + name: 'fragFINN e.V.' + url: 'https://www.fragfinn.de/' + +- regex: 'Clickagy' + name: 'Clickagy' + category: 'Crawler' + url: 'https://www.clickagy.com/' + producer: + name: 'Clickagy, LLC' + url: 'https://www.clickagy.com/' + +- regex: 'kiwitcms-gitops/[\d.]+' + name: 'Kiwi TCMS GitOps' + category: 'Service Agent' + url: 'https://kiwitcms.org' + producer: + name: 'Open Technologies Bulgaria, Ltd.' + url: 'https://kiwitcms.org' + +- regex: 'webtru_crawler' + name: 'webtru' + category: 'Crawler' + url: 'https://webtru.io/' + producer: + name: 'DataSign Inc.' + url: 'https://datasign.jp/' + +- regex: 'URLSuMaBot' + name: 'URLSuMaBot' + category: 'Crawler' + url: 'https://www.urlsuma.de/' + +- regex: '360JK yunjiankong' + name: '360JK' + category: 'Site Monitor' + url: 'http://jk.cloud.360.cn/' + producer: + name: '360 Security Technology Inc.' + url: 'https://www.360.cn/' + +- regex: 'UCSBNetworkMeasurement' + name: 'UCSB Network Measurement' + category: 'Crawler' + url: 'https://www.it.ucsb.edu/' + producer: + name: 'University of California, Santa Barbara' + url: 'https://www.it.ucsb.edu/' + +- regex: 'Plesk screenshot bot' + name: 'Plesk Screenshot Service' + category: 'Service Agent' + url: 'https://support.plesk.com/hc/en-us/articles/13302778306199-What-is-Plesk-Screenshot-Service' + producer: + name: 'Plesk International GmbH' + url: 'https://www.plesk.com/' + +- regex: 'Who\.is' + name: 'Who.is Bot' + category: 'Crawler' + url: 'https://who.is/' + +- regex: 'Probely' + name: 'Probely' + category: 'Security Checker' + url: 'https://probely.com/sos/' + producer: + name: 'Probely - Soluções de Cibersegurança, S.A.' + url: 'https://probely.com/' + +- regex: 'Uptimia(?:/[\d.]+)?' + name: 'Uptimia' + category: 'Site Monitor' + url: 'https://www.uptimia.com/' + producer: + name: 'JJ Online GmbH' + url: 'https://www.uptimia.com/' + +- regex: '2GDPR/[\d.]+' + name: '2GDPR' + category: 'Service Agent' + url: 'https://2gdpr.com/tos' + producer: + name: '2GDPR' + url: 'https://2gdpr.com/' + +- regex: 'abuse\.xmco\.fr' + name: 'Serenety' + category: 'Security Checker' + url: 'https://abuse.xmco.fr/' + producer: + name: 'XMCO, SASU' + url: 'https://www.xmco.fr/' + +- regex: 'CheckHost' + name: 'CheckHost' + category: 'Site Monitor' + url: 'https://check-host.net/' + producer: + name: 'CheckHost' + url: 'https://check-host.net/' + +- regex: 'LAC_IAHarvester/[\d.]+' + name: 'LAC IA Harvester' + category: 'Crawler' + url: 'https://library-archives.canada.ca/eng/services/government-canada/web-social-media-preservation-program/Pages/web-archive.aspx' + producer: + name: 'Library and Archives Canada' + url: 'https://library-archives.canada.ca/' + +- regex: 'InsytfulBot/[\d.]+' + name: 'InsytfulBot' + category: 'Crawler' + url: 'https://www.insytful.com/' + producer: + name: 'Zengenti Limited' + url: 'https://www.zengenti.com/' + +- regex: 'statista\.com' + name: 'Statista' + category: 'Crawler' + url: 'https://www.statista.com/' + producer: + name: 'Statista, Inc.' + url: 'https://www.statista.com/' + +- regex: 'SubstackContentFetch/[\d.]+' + name: 'Substack Content Fetch' + category: 'Crawler' + url: 'https://substack.com/' + producer: + name: 'Substack, Inc.' + url: 'https://substack.com/' + +- regex: '^ds9' + name: 'Deep SEARCH 9' + category: 'Crawler' + url: 'https://www.copyright.com/blog/ccc-expands-corporate-solutions-offering-with-new-technology/' + producer: + name: 'Copyright Clearance Center, Inc.' + url: 'https://www.copyright.com/' + +- regex: 'LiveJournal\.com' + name: 'LiveJournal' + url: 'https://www.livejournal.com/' + category: 'Feed Fetcher' + producer: + name: 'ООО "СИМ"' + url: 'https://www.livejournal.com/' + +- regex: 'bitdiscovery' + name: 'Tenable.asm' + category: 'Security Checker' + url: 'https://bitdiscovery.com/' + producer: + name: 'Tenable, Inc.' + url: 'https://www.tenable.com/' + +- regex: 'Castopod/[\d.]+' + name: 'Castopod' + category: 'Crawler' + url: 'https://www.castopod.org/' + +- regex: 'Elastic/Synthetics' + name: 'Elastic Synthetics' + category: 'Site Monitor' + url: 'https://github.com/elastic/synthetics' + producer: + name: 'Elasticsearch B.V.' + url: 'https://www.elastic.co/' + +- regex: 'WDG_Validator/[\d.]+' + name: 'WDG HTML Validator' + category: 'Validator' + url: 'http://www.htmlhelp.com/tools/validator/' + +- regex: 'scan@aegis.network' + name: 'Aegis' + category: 'Crawler' + url: 'https://web.archive.org/web/20180910002802/http://www.aegis.network/' + +- regex: 'CrawlyProjectCrawler/[\d.]+' + name: 'Crawly Project' + category: 'Crawler' + url: 'https://web.archive.org/web/20240326141952/https://crawlyproject.digitaldragon.dev/' + +- regex: 'BDFetch' + name: 'BDFetch' + category: 'Crawler' + url: 'https://web.archive.org/web/20130821043949/http://www.branddimensions.com/' + +- regex: 'PunkMap' + name: 'Punk Map' + category: 'Security Checker' + url: 'https://github.com/openeasm/punkmap' + +- regex: 'GenomeCrawlerd/[\d.]+' + name: 'Deepfield Genome' + category: 'Crawler' + url: 'https://www.nokia.com/networks/ip-networks/deepfield/genome/' + producer: + name: 'Nokia Corporation' + url: 'https://www.nokia.com/' + +- regex: 'Gaisbot/[\d.]+' + name: 'Gaisbot' + category: 'Crawler' + url: 'https://web.archive.org/web/20090604121511/https://gais.cs.ccu.edu.tw/robot.php' + +- regex: 'FAST-WebCrawler/[\d.]+' + name: 'AlltheWeb' + category: 'Crawler' + url: 'https://web.archive.org/web/20041020050801/http://www.alltheweb.com/help/webmaster/crawler' + +- regex: 'ducks\.party' + name: 'ducks.party' + category: 'Security Checker' + url: 'https://ducks.party/' + +# Generic bots +- regex: 'nuhk|grub-client|Download Demon|SearchExpress|Microsoft URL Control|borg|altavista|dataminr\.com|teoma|oegp|http%20client|htdig|mogimogi|larbin|scrubby|searchsight|semanticdiscovery|snappy|vortex(?!(?: Build|Plus| CM62| HD65))|zeal(?!ot)|dataparksearch|findlinks|BrowserMob|URL2PNG|ZooShot|GomezA|Google SketchUp|Read%20Later|7Siters|centuryb\.o\.t9|InterNaetBoten|EasyBib AutoCite|Bidtellect|tomnomnom/meg|cortex|Re-re Studio|adreview|AHC/|NameOfAgent|Request-Promise|ALittle Client|Hello,? world|wp_is_mobile|0xAbyssalDoesntExist|Anarchy99|^revolt|nvd0rz|xfa1|Hakai|gbrmss|fuck-your-hp|IDBTE4M CODE87|Antoine|Insomania|Hells-Net|b3astmode|Linux Gnu \(cow\)|Test Certificate Info|iplabel|Magellan|TheSafex?Internetx?Search|Searcherweb|kirkland-signature|LinkChain|survey-security-dot-txt|infrawatch|Time/|r00ts3c-owned-you|nvdorz|Root Slut|NiggaBalls|BotPoke|^xenu|^(?:chrome|firefox|Abcd|Dark|KvshClient|url|Zeus|ZmEu)$' name: 'Generic Bot' -- regex: '[a-z0-9_-]*(?:(? assert_error(@errors.multiple_snippets) end + @no_src_scripts """ + + + + + + Hello + + + + """ + test "no src attr doesn't count as snippet" do + stub_fetch_body(200, @no_src_scripts) + stub_installation(200, plausible_installed(false)) + + run_checks() + |> Checks.interpret_diagnostics() + |> assert_error(@errors.no_snippet) + end + @many_snippets_ok """