Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: copy and cut autocapture #1047

Merged
merged 14 commits into from
Mar 5, 2024
47 changes: 47 additions & 0 deletions playground/selection-autocapture/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script src="../../dist/array.js"></script>
<script>
posthog.init('sTMFPsFhdP1Ssg', {
api_host: 'http://127.0.0.1:8000',
autocapture_text_selection: true,
debug: true,
persistence: 'memory',
loaded: function(posthog) {
posthog.identify('test')
},
_onCapture: (event, eventData) => {
if (event === '$selection-autocapture') {
const selectionType = eventData.properties['$selection_type']
const selectionText = eventData.properties['$selection']
document.getElementById('selection-type-outlet').innerText = selectionType
document.getElementById('selection-text-outlet').innerText = selectionText
}
},
})
</script>
<h2>Demo site</h2>
<button>Here's a button you could click if you want to</button><br /><br />
<body>
<div>
<a href="https://posthog.com">Here's a link to the outside world</a>
</div>
<div id="shadow"></div>

<div style="padding: 2rem;background:green;display:none;color:white" id="feature"><h2>Look, a new beta feature</h2>
</div>

<div>
<h2>Result of selection autocapture</h2>
<p>Selection type is:
<div id="selection-type-outlet"></div>
</p>
<p>Selected text is:
<div id="selection-text-outlet"></div>
</p>
</div>
<script>
const header = document.createElement('header')
const shadowRoot = header.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = '<div style="border: black 1px solid"><h2>shadowroot</h2><input value="some input text">bla</input></div>'
document.body.prepend(header)
</script>
</body>
30 changes: 20 additions & 10 deletions src/autocapture-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ export function getClassNames(el: Element): string[] {
return splitClassString(className)
}

export function makeSafeText(s: string | null | undefined): string | null {
if (_isNullish(s)) {
return null
}

return (
_trim(s)
// scrub potentially sensitive values
.split(/(\s+)/)
.filter(shouldCaptureValue)
.join('')
// normalize whitespace
.replace(/[\r\n]/g, ' ')
.replace(/[ ]+/g, ' ')
// truncate
.substring(0, 255)
)
}

/*
* Get the direct text content of an element, protecting against sensitive data collection.
* Concats textContent of each of the element's text node children; this avoids potential
Expand All @@ -48,16 +67,7 @@ export function getSafeText(el: Element): string {
if (shouldCaptureElement(el) && !isSensitiveElement(el) && el.childNodes && el.childNodes.length) {
_each(el.childNodes, function (child) {
if (isTextNode(child) && child.textContent) {
elText += _trim(child.textContent)
// scrub potentially sensitive values
.split(/(\s+)/)
.filter(shouldCaptureValue)
.join('')
// normalize whitespace
.replace(/[\r\n]/g, ' ')
.replace(/[ ]+/g, ' ')
// truncate
.substring(0, 255)
elText += makeSafeText(child.textContent) ?? ''
}
})
}
Expand Down
89 changes: 89 additions & 0 deletions src/extensions/selection-autocapture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { PostHog } from '../posthog-core'
pauldambra marked this conversation as resolved.
Show resolved Hide resolved
import { _register_event } from '../utils'
import { document, window } from '../utils/globals'
import { logger } from '../utils/logger'
import { _isFunction } from '../utils/type-utils'
import { makeSafeText } from '../autocapture-utils'

const LEFT = 'Left'
const RIGHT = 'Right'
const UP = 'Up'
const DOWN = 'Down'
const ARROW = 'Arrow'
const PAGE = 'Page'
const ARROW_LEFT = ARROW + LEFT
const ARROW_RIGHT = ARROW + RIGHT
const ARROW_UP = ARROW + UP
const ARROW_DOWN = ARROW + DOWN
const PAGE_UP = PAGE + UP
const PAGE_DOWN = PAGE + DOWN

const navigationKeys = [
ARROW_UP,
ARROW_DOWN,
ARROW_LEFT,
ARROW_RIGHT,
PAGE_UP,
PAGE_DOWN,
'Home',
'End',
LEFT,
RIGHT,
UP,
DOWN,
'a', // select all
'A', // select all
]

const MOUSE = 'mouse'
const MOUSE_UP = MOUSE + 'up'
const KEY_UP = 'key' + 'up'

const debounce = (fn: any, ms = 50) => {
if (!_isFunction(fn)) {
return fn
}

let timeoutId: ReturnType<typeof setTimeout>
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), ms)
}
}

export const initSelectionAutocapture = (posthog: PostHog) => {
if (!document || !window) {
logger.info('document not available, selection autocapture not initialized')
return
}

const captureSelection = debounce((selectionType: string, selection: string): void => {
posthog.capture('$selection-autocapture', {
$selection_type: selectionType,
$selection: selection,
})
}, 150)

const handler = (event: Event) => {
let selectionType = 'unknown'
if (event.type === KEY_UP) {
selectionType = 'keyboard'
// only react to a navigation key that could have changed the selection
// e.g. don't react when someone releases ctrl or shift
const keyEvent = event as KeyboardEvent
if (navigationKeys.indexOf(keyEvent.key) === -1) {
return
}
} else if (event.type === MOUSE_UP) {
selectionType = MOUSE
}
const selection = window?.getSelection()
const selectedContent = makeSafeText(selection?.toString())
if (selectedContent) {
captureSelection(selectionType, selectedContent)
}
}

_register_event(document, MOUSE_UP, handler, false, true)
_register_event(document, KEY_UP, handler, false, true)
pauldambra marked this conversation as resolved.
Show resolved Hide resolved
}
5 changes: 5 additions & 0 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { document, userAgent } from './utils/globals'
import { SessionPropsManager } from './session-props'
import { _isBlockedUA } from './utils/blocked-uas'
import { SUPPORTS_REQUEST } from './utils/request-utils'
import { initSelectionAutocapture } from './extensions/selection-autocapture'

/*
SIMPLE STYLE GUIDE:
Expand Down Expand Up @@ -246,6 +247,10 @@ const create_phlib = function (
}
}

if (instance.config.autocapture_text_selection) {
initSelectionAutocapture(instance)
}

// if any instance on the page has debug = true, we set the
// global debug to be true
Config.DEBUG = Config.DEBUG || instance.config.debug
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export interface PostHogConfig {
disable_scroll_properties?: boolean
// Let the pageview scroll stats use a custom css selector for the root element, e.g. `main`
scroll_root_selector?: string | string[]
autocapture_text_selection?: boolean | string[]
}

export interface OptInOutCapturingOptions {
Expand Down
Loading