feat(tracker): add zustand support
This commit is contained in:
parent
3d82a6558b
commit
f360d8416d
29 changed files with 690 additions and 34 deletions
|
|
@ -2,7 +2,7 @@
|
|||
package messages
|
||||
|
||||
func IsReplayerType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 22 == id || 37 == id || 38 == id || 39 == id || 40 == id || 41 == id || 44 == id || 45 == id || 46 == id || 47 == id || 48 == id || 49 == id || 54 == id || 55 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 79 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
}
|
||||
|
||||
func IsIOSType(id int) bool {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ const (
|
|||
|
||||
MsgAdoptedSSRemoveOwner = 77
|
||||
|
||||
MsgZustand = 79
|
||||
|
||||
MsgIOSBatchMeta = 107
|
||||
|
||||
MsgIOSSessionStart = 90
|
||||
|
|
@ -3038,6 +3040,40 @@ func (msg *AdoptedSSRemoveOwner) TypeID() int {
|
|||
return 77
|
||||
}
|
||||
|
||||
type Zustand struct {
|
||||
message
|
||||
Mutation string
|
||||
State string
|
||||
}
|
||||
|
||||
func (msg *Zustand) Encode() []byte {
|
||||
buf := make([]byte, 21+len(msg.Mutation)+len(msg.State))
|
||||
buf[0] = 79
|
||||
p := 1
|
||||
p = WriteString(msg.Mutation, buf, p)
|
||||
p = WriteString(msg.State, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *Zustand) EncodeWithIndex() []byte {
|
||||
encoded := msg.Encode()
|
||||
if IsIOSType(msg.TypeID()) {
|
||||
return encoded
|
||||
}
|
||||
data := make([]byte, len(encoded)+8)
|
||||
copy(data[8:], encoded[:])
|
||||
binary.LittleEndian.PutUint64(data[0:], msg.Meta().Index)
|
||||
return data
|
||||
}
|
||||
|
||||
func (msg *Zustand) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *Zustand) TypeID() int {
|
||||
return 79
|
||||
}
|
||||
|
||||
type IOSBatchMeta struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
|
|
|
|||
|
|
@ -1306,6 +1306,18 @@ func DecodeAdoptedSSRemoveOwner(reader io.Reader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeZustand(reader io.Reader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &Zustand{}
|
||||
if msg.Mutation, err = ReadString(reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.State, err = ReadString(reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIOSBatchMeta(reader io.Reader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IOSBatchMeta{}
|
||||
|
|
@ -1939,6 +1951,9 @@ func ReadMessage(t uint64, reader io.Reader) (Message, error) {
|
|||
case 77:
|
||||
return DecodeAdoptedSSRemoveOwner(reader)
|
||||
|
||||
case 79:
|
||||
return DecodeZustand(reader)
|
||||
|
||||
case 107:
|
||||
return DecodeIOSBatchMeta(reader)
|
||||
|
||||
|
|
|
|||
|
|
@ -63,13 +63,6 @@ class SessionStart(Message):
|
|||
self.user_id = user_id
|
||||
|
||||
|
||||
class SessionDisconnect(Message):
|
||||
__id__ = 2
|
||||
|
||||
def __init__(self, timestamp):
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
class SessionEnd(Message):
|
||||
__id__ = 3
|
||||
|
||||
|
|
@ -106,7 +99,6 @@ class CreateDocument(Message):
|
|||
__id__ = 7
|
||||
|
||||
def __init__(self, ):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
|
@ -752,6 +744,14 @@ class AdoptedSSRemoveOwner(Message):
|
|||
self.id = id
|
||||
|
||||
|
||||
class Zustand(Message):
|
||||
__id__ = 79
|
||||
|
||||
def __init__(self, mutation, state):
|
||||
self.mutation = mutation
|
||||
self.state = state
|
||||
|
||||
|
||||
class IOSBatchMeta(Message):
|
||||
__id__ = 107
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from msgcodec.codec import Codec
|
||||
from msgcodec.messages import *
|
||||
from typing import List
|
||||
import io
|
||||
|
||||
class MessageCodec(Codec):
|
||||
|
|
@ -42,7 +43,7 @@ class MessageCodec(Codec):
|
|||
raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}")
|
||||
return decoded
|
||||
|
||||
def decode_detailed(self, b: bytes):
|
||||
def decode_detailed(self, b: bytes) -> List[Message]:
|
||||
reader = io.BytesIO(b)
|
||||
messages_list = list()
|
||||
messages_list.append(self.handler(reader, 0))
|
||||
|
|
@ -61,7 +62,7 @@ class MessageCodec(Codec):
|
|||
break
|
||||
return messages_list
|
||||
|
||||
def handler(self, reader: io.BytesIO, mode=0):
|
||||
def handler(self, reader: io.BytesIO, mode=0) -> Message:
|
||||
message_id = self.read_message_id(reader)
|
||||
if mode == 1:
|
||||
# We skip the three bytes representing the length of message. It can be used to skip unwanted messages
|
||||
|
|
@ -71,9 +72,10 @@ class MessageCodec(Codec):
|
|||
# Old format with no bytes for message length
|
||||
return self.read_head_message(reader, message_id)
|
||||
else:
|
||||
raise IOError()
|
||||
raise IOError()
|
||||
|
||||
def read_head_message(self, reader: io.BytesIO, message_id) -> Message:
|
||||
|
||||
def read_head_message(self, reader: io.BytesIO, message_id: int):
|
||||
if message_id == 80:
|
||||
return BatchMeta(
|
||||
page_no=self.read_uint(reader),
|
||||
|
|
@ -121,11 +123,6 @@ class MessageCodec(Codec):
|
|||
user_id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 2:
|
||||
return SessionDisconnect(
|
||||
timestamp=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 3:
|
||||
return SessionEnd(
|
||||
timestamp=self.read_uint(reader)
|
||||
|
|
@ -665,6 +662,12 @@ class MessageCodec(Codec):
|
|||
id=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 79:
|
||||
return Zustand(
|
||||
mutation=self.read_string(reader),
|
||||
state=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 107:
|
||||
return IOSBatchMeta(
|
||||
timestamp=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -527,6 +527,16 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 79: {
|
||||
const mutation = this.readString(); if (mutation === null) { return resetPointer() }
|
||||
const state = this.readString(); if (state === null) { return resetPointer() }
|
||||
return {
|
||||
tp: "zustand",
|
||||
mutation,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
case 90: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const projectID = this.readUint(); if (projectID === null) { return resetPointer() }
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import type {
|
|||
RawAdoptedSsDeleteRule,
|
||||
RawAdoptedSsAddOwner,
|
||||
RawAdoptedSsRemoveOwner,
|
||||
RawZustand,
|
||||
RawIosSessionStart,
|
||||
RawIosCustomEvent,
|
||||
RawIosScreenChanges,
|
||||
|
|
@ -147,6 +148,8 @@ export type AdoptedSsAddOwner = RawAdoptedSsAddOwner & Timed
|
|||
|
||||
export type AdoptedSsRemoveOwner = RawAdoptedSsRemoveOwner & Timed
|
||||
|
||||
export type Zustand = RawZustand & Timed
|
||||
|
||||
export type IosSessionStart = RawIosSessionStart & Timed
|
||||
|
||||
export type IosCustomEvent = RawIosCustomEvent & Timed
|
||||
|
|
|
|||
|
|
@ -300,6 +300,12 @@ export interface RawAdoptedSsRemoveOwner {
|
|||
id: number,
|
||||
}
|
||||
|
||||
export interface RawZustand {
|
||||
tp: "zustand",
|
||||
mutation: string,
|
||||
state: string,
|
||||
}
|
||||
|
||||
export interface RawIosSessionStart {
|
||||
tp: "ios_session_start",
|
||||
timestamp: number,
|
||||
|
|
@ -371,4 +377,4 @@ export interface RawIosNetworkCall {
|
|||
}
|
||||
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawConnectionInformation | RawSetPageVisibility | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawConnectionInformation | RawSetPageVisibility | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export const TP_MAP = {
|
|||
75: "adopted_ss_delete_rule",
|
||||
76: "adopted_ss_add_owner",
|
||||
77: "adopted_ss_remove_owner",
|
||||
79: "zustand",
|
||||
90: "ios_session_start",
|
||||
93: "ios_custom_event",
|
||||
96: "ios_screen_changes",
|
||||
|
|
|
|||
|
|
@ -382,8 +382,14 @@ type TrAdoptedSSRemoveOwner = [
|
|||
id: number,
|
||||
]
|
||||
|
||||
type TrZustand = [
|
||||
type: 79,
|
||||
mutation: string,
|
||||
state: string,
|
||||
]
|
||||
|
||||
export type TrackerMessage = TrBatchMetadata | TrPartitionedMessage | TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrJSException | TrRawCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner
|
||||
|
||||
export type TrackerMessage = TrBatchMetadata | TrPartitionedMessage | TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrJSException | TrRawCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrZustand
|
||||
|
||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||
switch(tMsg[0]) {
|
||||
|
|
@ -750,6 +756,14 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 79: {
|
||||
return {
|
||||
tp: "zustand",
|
||||
mutation: tMsg[1],
|
||||
state: tMsg[2],
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Special one for Batch Metadata. Message id could define the version
|
||||
# Special one for Batch Metadata. Message id could define the version
|
||||
|
||||
# Depricated since tracker 3.6.0 in favor of BatchMetadata
|
||||
message 80, 'BatchMeta', :replayer => false, :tracker => false do
|
||||
message 80, 'BatchMeta', :replayer => false, :tracker => false do
|
||||
uint 'PageNo'
|
||||
uint 'FirstIndex'
|
||||
int 'Timestamp'
|
||||
|
|
@ -421,7 +421,7 @@ message 70, 'CreateIFrameDocument' do
|
|||
uint 'FrameID'
|
||||
uint 'ID'
|
||||
end
|
||||
|
||||
|
||||
#Since 3.6.0 AdoptedStyleSheets
|
||||
message 71, 'AdoptedSSReplaceURLBased' do
|
||||
uint 'SheetID'
|
||||
|
|
@ -432,26 +432,31 @@ message 72, 'AdoptedSSReplace', :tracker => false do
|
|||
uint 'SheetID'
|
||||
string 'Text'
|
||||
end
|
||||
message 73, 'AdoptedSSInsertRuleURLBased' do
|
||||
message 73, 'AdoptedSSInsertRuleURLBased' do
|
||||
uint 'SheetID'
|
||||
string 'Rule'
|
||||
uint 'Index'
|
||||
string 'BaseURL'
|
||||
end
|
||||
message 74, 'AdoptedSSInsertRule', :tracker => false do
|
||||
message 74, 'AdoptedSSInsertRule', :tracker => false do
|
||||
uint 'SheetID'
|
||||
string 'Rule'
|
||||
uint 'Index'
|
||||
end
|
||||
message 75, 'AdoptedSSDeleteRule' do
|
||||
message 75, 'AdoptedSSDeleteRule' do
|
||||
uint 'SheetID'
|
||||
uint 'Index'
|
||||
end
|
||||
message 76, 'AdoptedSSAddOwner' do
|
||||
message 76, 'AdoptedSSAddOwner' do
|
||||
uint 'SheetID'
|
||||
uint 'ID'
|
||||
end
|
||||
message 77, 'AdoptedSSRemoveOwner' do
|
||||
message 77, 'AdoptedSSRemoveOwner' do
|
||||
uint 'SheetID'
|
||||
uint 'ID'
|
||||
end
|
||||
|
||||
message 79, 'Zustand' do
|
||||
string 'Mutation'
|
||||
string 'State'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-vuex",
|
||||
"description": "Tracker plugin for Vuex state recording",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"keywords": [
|
||||
"vuex",
|
||||
"logging",
|
||||
|
|
@ -23,8 +23,7 @@
|
|||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@openreplay/tracker": "^3.4.8",
|
||||
"@ngrx/store": ">=4"
|
||||
"@openreplay/tracker": "^3.4.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openreplay/tracker": "^3.4.8",
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default function(opts: Partial<Options> = {}) {
|
|||
const randomId = Math.random().toString(36).substring(2, 9)
|
||||
store.subscribe((mutation, storeState) => {
|
||||
state[storeName || randomId] = state
|
||||
processMutationAndState(app, options, encoder, mutation, storeState);
|
||||
processMutationAndState(app, options, encoder, mutation, state);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
6
tracker/tracker-zustand/.gitignore
vendored
Normal file
6
tracker/tracker-zustand/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
lib
|
||||
cjs
|
||||
.cache
|
||||
*.DS_Store
|
||||
5
tracker/tracker-zustand/.npmignore
Normal file
5
tracker/tracker-zustand/.npmignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
src
|
||||
tsconfig-cjs.json
|
||||
tsconfig.json
|
||||
.prettierrc.json
|
||||
.cache
|
||||
19
tracker/tracker-zustand/LICENSE
Normal file
19
tracker/tracker-zustand/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 Asayer, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
48
tracker/tracker-zustand/README.md
Normal file
48
tracker/tracker-zustand/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# OpenReplay Tracker Vuex plugin
|
||||
A Vuex plugin for OpenReplay Tracker. This plugin allows you to see the application state during session replay.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
npm i @openreplay/tracker-vuex
|
||||
```
|
||||
|
||||
## Usage
|
||||
Initialize the `@openreplay/tracker` package as usual and load the plugin into it.
|
||||
Then put the generated plugin into your `plugins` field of your store.
|
||||
|
||||
```js
|
||||
import Vuex from 'vuex'
|
||||
import Tracker from '@openreplay/tracker';
|
||||
import trackerVuex from '@openreplay/tracker-vuex';
|
||||
|
||||
const tracker = new Tracker({
|
||||
projectKey: YOUR_PROJECT_KEY,
|
||||
});
|
||||
|
||||
const store = new Vuex.Store({
|
||||
// ...
|
||||
plugins: [tracker.plugin(trackerVuex())],
|
||||
});
|
||||
```
|
||||
|
||||
You can customize the plugin behavior with options to sanitize your data. They are similar to the ones from the standard `createLogger` plugin.
|
||||
|
||||
```js
|
||||
trackerVuex({
|
||||
filter (mutation, state) {
|
||||
// returns `true` if a mutation should be logged
|
||||
// `mutation` is a `{ type, payload }`
|
||||
return mutation.type !== "aBlacklistedMutation";
|
||||
},
|
||||
transformer (state) {
|
||||
// transform the state before logging it.
|
||||
// for example return only a specific sub-tree
|
||||
return state.subTree;
|
||||
},
|
||||
mutationTransformer (mutation) {
|
||||
// mutations are logged in the format of `{ type, payload }`
|
||||
// we can format it any way we want.
|
||||
return mutation.type;
|
||||
},
|
||||
})
|
||||
```
|
||||
35
tracker/tracker-zustand/package.json
Normal file
35
tracker/tracker-zustand/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-zustand",
|
||||
"description": "Tracker plugin for Zustand state recording",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"zustand",
|
||||
"state",
|
||||
"logging",
|
||||
"replay"
|
||||
],
|
||||
"author": "Nikita Melnikov <nikita@openreplay.com>",
|
||||
"contributors": [
|
||||
"Aleksandr K <alex@openreplay.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
"scripts": {
|
||||
"lint": "prettier --write 'src/**/*.ts' && tsc --noEmit",
|
||||
"build": "npm run build-es && npm run build-cjs",
|
||||
"build-es": "rm -Rf lib && tsc",
|
||||
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs'",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@openreplay/tracker": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openreplay/tracker": "file:../tracker",
|
||||
"prettier": "^1.18.2",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"typescript": "^4.6.0-dev.20211126"
|
||||
}
|
||||
}
|
||||
62
tracker/tracker-zustand/src/index.ts
Normal file
62
tracker/tracker-zustand/src/index.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { App, Messages } from "@openreplay/tracker";
|
||||
import { Encoder, sha1 } from "./syncod/index.js";
|
||||
|
||||
export interface Options {
|
||||
filter: (mutation: any, state: any) => boolean;
|
||||
transformer: (state: any) => any;
|
||||
mutationTransformer: (mutation: any) => any;
|
||||
}
|
||||
|
||||
function processMutationAndState(
|
||||
app: App,
|
||||
options: Options,
|
||||
encoder: Encoder,
|
||||
mutation: string[],
|
||||
state: Record<string, any>
|
||||
) {
|
||||
if (options.filter(mutation, state)) {
|
||||
try {
|
||||
const _mutation = encoder.encode(options.mutationTransformer(mutation));
|
||||
const _state = encoder.encode(options.transformer(state));
|
||||
const _table = encoder.commit();
|
||||
for (let key in _table) app.send(Messages.OTable(key, _table[key]));
|
||||
app.send(Messages.Zustand(_mutation, _state));
|
||||
} catch (e) {
|
||||
encoder.clear();
|
||||
app.debug.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function(opts: Partial<Options> = {}) {
|
||||
const options: Options = Object.assign(
|
||||
{
|
||||
filter: () => true,
|
||||
transformer: state => state,
|
||||
mutationTransformer: mutation => mutation,
|
||||
},
|
||||
opts
|
||||
);
|
||||
return (app: App | null) => {
|
||||
if (app === null) {
|
||||
return Function.prototype;
|
||||
}
|
||||
const encoder = new Encoder(sha1, 50);
|
||||
const state = {};
|
||||
return (storeName: string = Math.random().toString(36).substring(2, 9)) =>
|
||||
(config: Function) =>
|
||||
(set: (...args: any) => void, get: () => Record<string, any>, api: any) =>
|
||||
config(
|
||||
(...args) => {
|
||||
set(...args)
|
||||
const newState = get();
|
||||
state[storeName] = newState
|
||||
const triggeredActions = args.map(action => action.toString?.())
|
||||
|
||||
processMutationAndState(app, options, encoder, triggeredActions, state)
|
||||
},
|
||||
get,
|
||||
api
|
||||
)
|
||||
};
|
||||
}
|
||||
18
tracker/tracker-zustand/src/syncod/chars.ts
Normal file
18
tracker/tracker-zustand/src/syncod/chars.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const chars = {};
|
||||
|
||||
[
|
||||
"DEL",
|
||||
"UNDEF",
|
||||
"TRUE",
|
||||
"FALSE",
|
||||
"NUMBER",
|
||||
"BIGINT",
|
||||
"FUNCTION",
|
||||
"STRING",
|
||||
"SYMBOL",
|
||||
"NULL",
|
||||
"OBJECT",
|
||||
"ARRAY"
|
||||
].forEach((k, i) => (chars[k] = String.fromCharCode(i + 0xe000)));
|
||||
|
||||
export default chars;
|
||||
220
tracker/tracker-zustand/src/syncod/encoder.ts
Normal file
220
tracker/tracker-zustand/src/syncod/encoder.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import _ from "./chars.js";
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
export default class Encoder {
|
||||
// @ts-ignore
|
||||
constructor(hash, slen = Infinity) {
|
||||
// @ts-ignore
|
||||
this._hash = hash;
|
||||
// @ts-ignore
|
||||
this._slen = slen;
|
||||
// @ts-ignore
|
||||
this._refmap = new Map();
|
||||
// @ts-ignore
|
||||
this._refset = new Set();
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
_ref_str(str) {
|
||||
// @ts-ignore
|
||||
if (str.length < this._slen && str.indexOf(_.DEL) === -1) {
|
||||
// @ts-ignore
|
||||
return str;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
let ref = this._refmap.get(str);
|
||||
// @ts-ignore
|
||||
if (ref === undefined) {
|
||||
// @ts-ignore
|
||||
ref = this._hash(str);
|
||||
// @ts-ignore
|
||||
this._refmap.set(str, ref);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
return ref;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
_encode_prim(obj) {
|
||||
// @ts-ignore
|
||||
switch (typeof obj) {
|
||||
// @ts-ignore
|
||||
case "undefined":
|
||||
// @ts-ignore
|
||||
return _.UNDEF;
|
||||
// @ts-ignore
|
||||
case "boolean":
|
||||
// @ts-ignore
|
||||
return obj ? _.TRUE : _.FALSE;
|
||||
// @ts-ignore
|
||||
case "number":
|
||||
// @ts-ignore
|
||||
return _.NUMBER + obj.toString();
|
||||
// @ts-ignore
|
||||
case "bigint":
|
||||
// @ts-ignore
|
||||
return _.BIGINT + obj.toString();
|
||||
// @ts-ignore
|
||||
case "function":
|
||||
// @ts-ignore
|
||||
return _.FUNCTION;
|
||||
// @ts-ignore
|
||||
case "string":
|
||||
// @ts-ignore
|
||||
return _.STRING + this._ref_str(obj);
|
||||
// @ts-ignore
|
||||
case "symbol":
|
||||
// @ts-ignore
|
||||
return _.SYMBOL + this._ref_str(obj.toString().slice(7, -1));
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
if (obj === null) {
|
||||
// @ts-ignore
|
||||
return _.NULL;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
_encode_obj(obj, ref = this._refmap.get(obj)) {
|
||||
// @ts-ignore
|
||||
return (Array.isArray(obj) ? _.ARRAY : _.OBJECT) + ref;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
_encode_term(obj) {
|
||||
// @ts-ignore
|
||||
return this._encode_prim(obj) || this._encode_obj(obj);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
_encode_deep(obj, depth) {
|
||||
// @ts-ignore
|
||||
const enc = this._encode_prim(obj);
|
||||
// @ts-ignore
|
||||
if (enc !== undefined) {
|
||||
// @ts-ignore
|
||||
return enc;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
const ref = this._refmap.get(obj);
|
||||
// @ts-ignore
|
||||
switch (typeof ref) {
|
||||
// @ts-ignore
|
||||
case "number":
|
||||
// @ts-ignore
|
||||
return (depth - ref).toString();
|
||||
// @ts-ignore
|
||||
case "string":
|
||||
// @ts-ignore
|
||||
return this._encode_obj(obj, ref);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
this._refmap.set(obj, depth);
|
||||
// @ts-ignore
|
||||
const hash = this._hash(
|
||||
// @ts-ignore
|
||||
(Array.isArray(obj)
|
||||
// @ts-ignore
|
||||
? obj.map(v => this._encode_deep(v, depth + 1))
|
||||
// @ts-ignore
|
||||
: Object.keys(obj)
|
||||
// @ts-ignore
|
||||
.sort()
|
||||
// @ts-ignore
|
||||
.map(
|
||||
// @ts-ignore
|
||||
k =>
|
||||
// @ts-ignore
|
||||
this._ref_str(k) + _.DEL + this._encode_deep(obj[k], depth + 1)
|
||||
// @ts-ignore
|
||||
)
|
||||
// @ts-ignore
|
||||
).join(_.DEL)
|
||||
// @ts-ignore
|
||||
);
|
||||
// @ts-ignore
|
||||
this._refmap.set(obj, hash);
|
||||
// @ts-ignore
|
||||
return this._encode_obj(obj, hash);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
encode(obj) {
|
||||
// @ts-ignore
|
||||
return this._encode_deep(obj, 0);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
commit() {
|
||||
// @ts-ignore
|
||||
const dict = {};
|
||||
// @ts-ignore
|
||||
this._refmap.forEach((ref, obj) => {
|
||||
// @ts-ignore
|
||||
if (this._refset.has(ref)) {
|
||||
// @ts-ignore
|
||||
return;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
this._refset.add(ref);
|
||||
// @ts-ignore
|
||||
if (typeof obj !== "string") {
|
||||
// @ts-ignore
|
||||
obj = (Array.isArray(obj)
|
||||
// @ts-ignore
|
||||
? obj.map(v => this._encode_term(v))
|
||||
// @ts-ignore
|
||||
: Object.keys(obj).map(
|
||||
// @ts-ignore
|
||||
k => this._ref_str(k) + _.DEL + this._encode_term(obj[k])
|
||||
// @ts-ignore
|
||||
)
|
||||
// @ts-ignore
|
||||
).join(_.DEL);
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
dict[ref] = obj;
|
||||
// @ts-ignore
|
||||
});
|
||||
// @ts-ignore
|
||||
this._refmap.clear();
|
||||
// @ts-ignore
|
||||
return dict;
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
// @ts-ignore
|
||||
clear() {
|
||||
// @ts-ignore
|
||||
this._refmap.clear();
|
||||
// @ts-ignore
|
||||
this._refset.clear();
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
}
|
||||
// @ts-ignore
|
||||
5
tracker/tracker-zustand/src/syncod/index.ts
Normal file
5
tracker/tracker-zustand/src/syncod/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// TODO: SSR solution for all asayer libraries
|
||||
import Encoder from "./encoder.js";
|
||||
import sha1 from "./sha1.js";
|
||||
|
||||
export { Encoder, sha1 };
|
||||
104
tracker/tracker-zustand/src/syncod/sha1.ts
Normal file
104
tracker/tracker-zustand/src/syncod/sha1.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
|
||||
* in FIPS PUB 180-1
|
||||
* Version 2.1a Copyright Paul Johnston 2000 - 2002.
|
||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||
* Distributed under the BSD License
|
||||
* See http://pajhome.org.uk/crypt/md5 for details.
|
||||
*/
|
||||
|
||||
function core_sha1(x, len) {
|
||||
x[len >> 5] |= 0x80 << (24 - (len % 32));
|
||||
x[(((len + 64) >> 9) << 4) + 15] = len;
|
||||
|
||||
var w = Array(80);
|
||||
var a = 1732584193;
|
||||
var b = -271733879;
|
||||
var c = -1732584194;
|
||||
var d = 271733878;
|
||||
var e = -1009589776;
|
||||
|
||||
for (var i = 0; i < x.length; i += 16) {
|
||||
var olda = a;
|
||||
var oldb = b;
|
||||
var oldc = c;
|
||||
var oldd = d;
|
||||
var olde = e;
|
||||
|
||||
for (var j = 0; j < 80; j++) {
|
||||
if (j < 16) w[j] = x[i + j];
|
||||
else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
|
||||
var t = safe_add(
|
||||
safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
|
||||
safe_add(safe_add(e, w[j]), sha1_kt(j))
|
||||
);
|
||||
e = d;
|
||||
d = c;
|
||||
c = rol(b, 30);
|
||||
b = a;
|
||||
a = t;
|
||||
}
|
||||
|
||||
a = safe_add(a, olda);
|
||||
b = safe_add(b, oldb);
|
||||
c = safe_add(c, oldc);
|
||||
d = safe_add(d, oldd);
|
||||
e = safe_add(e, olde);
|
||||
}
|
||||
return Array(a, b, c, d, e);
|
||||
}
|
||||
|
||||
function sha1_ft(t, b, c, d) {
|
||||
if (t < 20) return (b & c) | (~b & d);
|
||||
if (t < 40) return b ^ c ^ d;
|
||||
if (t < 60) return (b & c) | (b & d) | (c & d);
|
||||
return b ^ c ^ d;
|
||||
}
|
||||
|
||||
function sha1_kt(t) {
|
||||
return t < 20
|
||||
? 1518500249
|
||||
: t < 40
|
||||
? 1859775393
|
||||
: t < 60
|
||||
? -1894007588
|
||||
: -899497514;
|
||||
}
|
||||
|
||||
function safe_add(x, y) {
|
||||
var lsw = (x & 0xffff) + (y & 0xffff);
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xffff);
|
||||
}
|
||||
|
||||
function rol(num, cnt) {
|
||||
return (num << cnt) | (num >>> (32 - cnt));
|
||||
}
|
||||
|
||||
function str2binb(str) {
|
||||
var bin = Array();
|
||||
var mask = (1 << 16) - 1;
|
||||
for (var i = 0; i < str.length * 16; i += 16)
|
||||
bin[i >> 5] |= (str.charCodeAt(i / 16) & mask) << (32 - 16 - (i % 32));
|
||||
return bin;
|
||||
}
|
||||
|
||||
function binb2b64(binarray) {
|
||||
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
var str = "";
|
||||
for (var i = 0; i < binarray.length * 4; i += 3) {
|
||||
var triplet =
|
||||
(((binarray[i >> 2] >> (8 * (3 - (i % 4)))) & 0xff) << 16) |
|
||||
(((binarray[(i + 1) >> 2] >> (8 * (3 - ((i + 1) % 4)))) & 0xff) << 8) |
|
||||
((binarray[(i + 2) >> 2] >> (8 * (3 - ((i + 2) % 4)))) & 0xff);
|
||||
for (var j = 0; j < 4; j++) {
|
||||
if (i * 8 + j * 6 <= binarray.length * 32)
|
||||
str += tab.charAt((triplet >> (6 * (3 - j))) & 0x3f);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export default function(s) {
|
||||
return binb2b64(core_sha1(str2binb(s), s.length * 16));
|
||||
}
|
||||
8
tracker/tracker-zustand/tsconfig-cjs.json
Normal file
8
tracker/tracker-zustand/tsconfig-cjs.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./cjs",
|
||||
"declaration": false
|
||||
},
|
||||
}
|
||||
12
tracker/tracker-zustand/tsconfig.json
Normal file
12
tracker/tracker-zustand/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"moduleResolution": "nodenext",
|
||||
"declaration": true,
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export declare const enum Type {
|
|||
AdoptedSSDeleteRule = 75,
|
||||
AdoptedSSAddOwner = 76,
|
||||
AdoptedSSRemoveOwner = 77,
|
||||
Zustand = 79,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -438,6 +439,12 @@ export type AdoptedSSRemoveOwner = [
|
|||
/*id:*/ number,
|
||||
]
|
||||
|
||||
export type Zustand = [
|
||||
/*type:*/ Type.Zustand,
|
||||
/*mutation:*/ string,
|
||||
/*state:*/ string,
|
||||
]
|
||||
|
||||
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner
|
||||
|
||||
type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSException | RawCustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | Zustand
|
||||
export default Message
|
||||
|
|
|
|||
|
|
@ -707,3 +707,14 @@ export function AdoptedSSRemoveOwner(
|
|||
]
|
||||
}
|
||||
|
||||
export function Zustand(
|
||||
mutation: string,
|
||||
state: string,
|
||||
): Messages.Zustand {
|
||||
return [
|
||||
Messages.Type.Zustand,
|
||||
mutation,
|
||||
state,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
|||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
case Messages.Type.Zustand:
|
||||
return this.string(msg[1]) && this.string(msg[2])
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue