├── .docker └── Dockerfile ├── .editorconfig ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── Taskfile.yml ├── api └── paginate.go ├── cmd └── cli │ ├── cli │ ├── cli.go │ └── config.go │ └── main.go ├── controller ├── api.go ├── app.go ├── bus.go └── container.go ├── docker-compose.yaml ├── event ├── context.go ├── contexts.go ├── contexts │ ├── action.go │ ├── alias.go │ ├── app.go │ ├── browser.go │ ├── campaign.go │ ├── connectivity.go │ ├── contexts.go │ ├── device.go │ ├── extra.go │ ├── group.go │ ├── library.go │ ├── location.go │ ├── network.go │ ├── os.go │ ├── page.go │ ├── referrer.go │ ├── screen.go │ ├── session.go │ ├── timing.go │ ├── user.go │ └── viewport.go ├── event.go ├── events │ ├── action.go │ ├── alias.go │ ├── events.go │ ├── group.go │ ├── identify.go │ ├── pageview.go │ ├── screen.go │ ├── session.go │ ├── timing.go │ └── transaction.go ├── options.go ├── registry.go ├── tests │ └── events_test.go └── validate.go ├── gen.go ├── go.mod ├── go.sum ├── lerna.json ├── modules ├── init │ └── init.go ├── modules.go ├── processor │ ├── enhancer │ │ ├── enhancer.go │ │ └── paths.go │ ├── fanin │ │ └── fanin.go │ └── sink │ │ └── sink.go ├── sink │ ├── loader │ │ ├── command │ │ │ └── track_event.go │ │ ├── entity │ │ │ └── rawevent.go │ │ ├── gen.go │ │ ├── loader.go │ │ ├── schema │ │ │ └── rawevent.go │ │ └── store │ │ │ ├── ent │ │ │ ├── client.go │ │ │ ├── config.go │ │ │ ├── context.go │ │ │ ├── ent.go │ │ │ ├── enttest │ │ │ │ └── enttest.go │ │ │ ├── hook │ │ │ │ └── hook.go │ │ │ ├── migrate │ │ │ │ ├── migrate.go │ │ │ │ └── schema.go │ │ │ ├── mutation.go │ │ │ ├── predicate │ │ │ │ └── predicate.go │ │ │ ├── privacy │ │ │ │ └── privacy.go │ │ │ ├── rawevent.go │ │ │ ├── rawevent │ │ │ │ ├── rawevent.go │ │ │ │ └── where.go │ │ │ ├── rawevent_create.go │ │ │ ├── rawevent_delete.go │ │ │ ├── rawevent_query.go │ │ │ ├── rawevent_update.go │ │ │ ├── runtime.go │ │ │ ├── runtime │ │ │ │ └── runtime.go │ │ │ └── tx.go │ │ │ └── store.go │ └── reporter │ │ ├── api.go │ │ ├── command │ │ ├── event_extract.go │ │ └── event_report.go │ │ ├── entity │ │ ├── action.go │ │ ├── aggregate.go │ │ ├── app.go │ │ ├── campaign.go │ │ ├── device.go │ │ ├── event.go │ │ ├── event_tx.go │ │ ├── group.go │ │ ├── pagestats.go │ │ ├── query.go │ │ ├── referrerstats.go │ │ ├── session.go │ │ ├── sitestats.go │ │ └── user.go │ │ ├── gen.go │ │ ├── live.go │ │ ├── reporter.go │ │ ├── schema │ │ ├── action.go │ │ ├── alias.go │ │ ├── app.go │ │ ├── browser.go │ │ ├── campaign.go │ │ ├── connectivity.go │ │ ├── device.go │ │ ├── event.go │ │ ├── extra.go │ │ ├── group.go │ │ ├── library.go │ │ ├── location.go │ │ ├── network.go │ │ ├── os.go │ │ ├── page.go │ │ ├── referrer.go │ │ ├── screen.go │ │ ├── session.go │ │ ├── timing.go │ │ ├── user.go │ │ └── viewport.go │ │ └── store │ │ ├── ent │ │ ├── action.go │ │ ├── action │ │ │ ├── action.go │ │ │ └── where.go │ │ ├── action_create.go │ │ ├── action_delete.go │ │ ├── action_query.go │ │ ├── action_update.go │ │ ├── alias.go │ │ ├── alias │ │ │ ├── alias.go │ │ │ └── where.go │ │ ├── alias_create.go │ │ ├── alias_delete.go │ │ ├── alias_query.go │ │ ├── alias_update.go │ │ ├── app.go │ │ ├── app │ │ │ ├── app.go │ │ │ └── where.go │ │ ├── app_create.go │ │ ├── app_delete.go │ │ ├── app_query.go │ │ ├── app_update.go │ │ ├── browser.go │ │ ├── browser │ │ │ ├── browser.go │ │ │ └── where.go │ │ ├── browser_create.go │ │ ├── browser_delete.go │ │ ├── browser_query.go │ │ ├── browser_update.go │ │ ├── campaign.go │ │ ├── campaign │ │ │ ├── campaign.go │ │ │ └── where.go │ │ ├── campaign_create.go │ │ ├── campaign_delete.go │ │ ├── campaign_query.go │ │ ├── campaign_update.go │ │ ├── client.go │ │ ├── config.go │ │ ├── connectivity.go │ │ ├── connectivity │ │ │ ├── connectivity.go │ │ │ └── where.go │ │ ├── connectivity_create.go │ │ ├── connectivity_delete.go │ │ ├── connectivity_query.go │ │ ├── connectivity_update.go │ │ ├── context.go │ │ ├── device.go │ │ ├── device │ │ │ ├── device.go │ │ │ └── where.go │ │ ├── device_create.go │ │ ├── device_delete.go │ │ ├── device_query.go │ │ ├── device_update.go │ │ ├── ent.go │ │ ├── enttest │ │ │ └── enttest.go │ │ ├── event.go │ │ ├── event │ │ │ ├── event.go │ │ │ └── where.go │ │ ├── event_create.go │ │ ├── event_delete.go │ │ ├── event_query.go │ │ ├── event_update.go │ │ ├── extra.go │ │ ├── extra │ │ │ ├── extra.go │ │ │ └── where.go │ │ ├── extra_create.go │ │ ├── extra_delete.go │ │ ├── extra_query.go │ │ ├── extra_update.go │ │ ├── group.go │ │ ├── group │ │ │ ├── group.go │ │ │ └── where.go │ │ ├── group_create.go │ │ ├── group_delete.go │ │ ├── group_query.go │ │ ├── group_update.go │ │ ├── hook │ │ │ └── hook.go │ │ ├── library.go │ │ ├── library │ │ │ ├── library.go │ │ │ └── where.go │ │ ├── library_create.go │ │ ├── library_delete.go │ │ ├── library_query.go │ │ ├── library_update.go │ │ ├── location.go │ │ ├── location │ │ │ ├── location.go │ │ │ └── where.go │ │ ├── location_create.go │ │ ├── location_delete.go │ │ ├── location_query.go │ │ ├── location_update.go │ │ ├── migrate │ │ │ ├── migrate.go │ │ │ └── schema.go │ │ ├── mutation.go │ │ ├── network.go │ │ ├── network │ │ │ ├── network.go │ │ │ └── where.go │ │ ├── network_create.go │ │ ├── network_delete.go │ │ ├── network_query.go │ │ ├── network_update.go │ │ ├── oscontext.go │ │ ├── oscontext │ │ │ ├── oscontext.go │ │ │ └── where.go │ │ ├── oscontext_create.go │ │ ├── oscontext_delete.go │ │ ├── oscontext_query.go │ │ ├── oscontext_update.go │ │ ├── page.go │ │ ├── page │ │ │ ├── page.go │ │ │ └── where.go │ │ ├── page_create.go │ │ ├── page_delete.go │ │ ├── page_query.go │ │ ├── page_update.go │ │ ├── predicate │ │ │ └── predicate.go │ │ ├── privacy │ │ │ └── privacy.go │ │ ├── referrer.go │ │ ├── referrer │ │ │ ├── referrer.go │ │ │ └── where.go │ │ ├── referrer_create.go │ │ ├── referrer_delete.go │ │ ├── referrer_query.go │ │ ├── referrer_update.go │ │ ├── runtime.go │ │ ├── runtime │ │ │ └── runtime.go │ │ ├── screen.go │ │ ├── screen │ │ │ ├── screen.go │ │ │ └── where.go │ │ ├── screen_create.go │ │ ├── screen_delete.go │ │ ├── screen_query.go │ │ ├── screen_update.go │ │ ├── session.go │ │ ├── session │ │ │ ├── session.go │ │ │ └── where.go │ │ ├── session_create.go │ │ ├── session_delete.go │ │ ├── session_query.go │ │ ├── session_update.go │ │ ├── timing.go │ │ ├── timing │ │ │ ├── timing.go │ │ │ └── where.go │ │ ├── timing_create.go │ │ ├── timing_delete.go │ │ ├── timing_query.go │ │ ├── timing_update.go │ │ ├── tx.go │ │ ├── user.go │ │ ├── user │ │ │ ├── user.go │ │ │ └── where.go │ │ ├── user_create.go │ │ ├── user_delete.go │ │ ├── user_query.go │ │ ├── user_update.go │ │ ├── viewport.go │ │ ├── viewport │ │ │ ├── viewport.go │ │ │ └── where.go │ │ ├── viewport_create.go │ │ ├── viewport_delete.go │ │ ├── viewport_query.go │ │ └── viewport_update.go │ │ └── reporter.go └── source │ ├── collector │ ├── collector.go │ └── tracker.go │ └── webhook │ ├── pingdom.go │ └── webhook.go ├── package.json ├── packages ├── core │ ├── .browserslistrc │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ ├── logo.png │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── Admin │ │ │ │ ├── Header.vue │ │ │ │ └── Page.vue │ │ │ ├── App │ │ │ │ ├── Bar.vue │ │ │ │ └── Nav.vue │ │ │ └── Widgets │ │ │ │ ├── Cards │ │ │ │ ├── Chart.vue │ │ │ │ └── Stat.vue │ │ │ │ └── Trend.vue │ │ ├── config │ │ │ └── app.config.json │ │ ├── layouts │ │ │ ├── Admin.vue │ │ │ ├── Page.vue │ │ │ └── index.js │ │ ├── main.js │ │ ├── plugins │ │ │ └── vuetify.js │ │ ├── router │ │ │ └── index.js │ │ ├── services │ │ │ └── plugins │ │ │ │ └── index.js │ │ ├── store │ │ │ └── index.js │ │ ├── styles │ │ │ └── variables.scss │ │ └── views │ │ │ ├── About.vue │ │ │ └── Home.vue │ └── vue.config.js ├── plugin │ ├── package.json │ └── src │ │ ├── index.js │ │ └── plugin.js ├── plugins │ └── example │ │ ├── package.json │ │ └── src │ │ ├── components │ │ └── HelloWorld.vue │ │ └── index.js ├── tracker │ ├── README.md │ ├── __tests__ │ │ └── tracker.test.js │ ├── package.json │ └── src │ │ ├── event │ │ └── event.js │ │ ├── index.js │ │ ├── queue.js │ │ ├── storage │ │ └── index.js │ │ └── tracker.js └── vue-tracker │ ├── .browserslistrc │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ └── HelloWorld.vue │ └── main.js ├── pkg ├── conv │ └── string.go └── netintf │ └── netintf.go ├── platform ├── bus │ ├── broker │ │ ├── broker.go │ │ ├── nats │ │ │ ├── embedded.go │ │ │ ├── nats.go │ │ │ └── options.go │ │ ├── nsq │ │ │ ├── embedded.go │ │ │ ├── nsq.go │ │ │ └── options.go │ │ └── options.go │ ├── bus.go │ ├── message │ │ ├── envelope.go │ │ └── message.go │ ├── options.go │ └── route.go ├── cache │ └── cache.go ├── config │ ├── bus.go │ ├── cache.go │ ├── config.go │ ├── database.go │ ├── file.go │ ├── logger.go │ ├── module.go │ ├── options.go │ ├── processor.go │ └── server.go ├── logger │ └── logger.go ├── platform.go ├── server │ └── http.go └── store │ ├── file..go │ ├── minio │ └── minio.go │ └── sql.go ├── processor ├── filter │ ├── condition.go │ └── filter.go ├── geoip │ ├── geoip.go │ └── geoip_test.go ├── init │ └── init.go ├── log │ └── log.go ├── plugin │ ├── example │ │ ├── .strana.yaml │ │ └── example.go │ ├── imports │ │ ├── github.com_blushft_strana_event.go │ │ ├── github.com_blushft_strana_event_contexts.go │ │ ├── github.com_blushft_strana_event_events.go │ │ ├── github.com_blushft_strana_processor.go │ │ └── imports.go │ ├── plugin.go │ └── plugin_test.go ├── processor.go └── useragent │ └── useragent.go ├── strana.go ├── tracker ├── examples │ └── basic │ │ └── main.go ├── options.go ├── store.go ├── store_test.go ├── timing.go ├── tracker.go └── tracker_test.go ├── xconfig.yaml ├── yarn-error.log └── yarn.lock /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine AS builder 2 | RUN apk add --no-cache gcc musl-dev 3 | 4 | WORKDIR /build 5 | COPY . . 6 | RUN go mod download 7 | RUN go generate ./... 8 | RUN go build -o strana ./cmd/cli/main.go 9 | 10 | FROM alpine 11 | LABEL maintainer="Blushft" 12 | 13 | RUN apk add ca-certificates 14 | 15 | WORKDIR /app 16 | COPY --from=builder /build/strana /app/strana 17 | 18 | EXPOSE 8863 19 | EXPOSE 4442 20 | EXPOSE 4443 21 | 22 | CMD ["/app/strana"] 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | 8 | [*.go] 9 | indent_size = 2 10 | indent_style = tab 11 | tab_width = 2 -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup Go 13 | uses: actions/setup-go@v2-beta 14 | with: 15 | go-version: 1.14 16 | id: go 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Run Tests 20 | run: go test -v ./... 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.dat 3 | node_modules 4 | .secrets 5 | .fixtures 6 | .docker/data/** 7 | .env 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false, 4 | "singleQuote": true, 5 | "endOfLine": "lf", 6 | "trailingComma": "es5", 7 | "printWidth": 120 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Config Command", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/cmd/cli/main.go", 13 | "cwd": "${workspaceFolder}", 14 | "args": ["config"] 15 | }, 16 | { 17 | "name": "Launch", 18 | "type": "go", 19 | "request": "launch", 20 | "mode": "auto", 21 | "program": "${workspaceFolder}/cmd/cli/main.go", 22 | "cwd": "${workspaceFolder}", 23 | "env": {}, 24 | "args": [] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.addTags": { 3 | "tags": "json,structs,mapstructure", 4 | "options": "json=omitempty,structs=omitempty,mapstructure=omitempty", 5 | "promptForTags": true, 6 | "transform": "camelcase" 7 | }, 8 | "go.lintTool": "golangci-lint" 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2020 BluShft 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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.] -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: '3' 4 | 5 | tasks: 6 | default: 7 | cmds: 8 | - task: build 9 | silent: true 10 | build: 11 | cmds: 12 | - go build -o strana ./cmd/cli/main.go 13 | docker-build: 14 | cmds: 15 | - docker build -t blushft/strana -f ./.docker/Dockerfile . 16 | -------------------------------------------------------------------------------- /api/paginate.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/gofiber/fiber" 8 | ) 9 | 10 | type Paginate struct { 11 | Offset int 12 | Limit int 13 | Start time.Time 14 | End time.Time 15 | } 16 | 17 | func defaultParams() *Paginate { 18 | return &Paginate{ 19 | Limit: 20, 20 | Offset: 0, 21 | Start: time.Now().AddDate(0, 0, -7), 22 | End: time.Now(), 23 | } 24 | } 25 | 26 | func apiParams(c *fiber.Ctx) { 27 | params := defaultParams() 28 | 29 | limit, err := strconv.Atoi(c.Query("limit")) 30 | if err == nil { 31 | params.Limit = limit 32 | } 33 | 34 | offset, err := strconv.Atoi(c.Query("offset")) 35 | if err == nil { 36 | params.Offset = offset 37 | } 38 | 39 | start, err := strconv.ParseInt(c.Query("start"), 10, 64) 40 | if err == nil { 41 | params.Start = time.Unix(start, 0) 42 | } 43 | 44 | end, err := strconv.ParseInt(c.Query("end"), 10, 64) 45 | if err == nil { 46 | params.End = time.Unix(end, 0) 47 | } 48 | 49 | c.Locals("params", params) 50 | 51 | c.Next() 52 | } 53 | -------------------------------------------------------------------------------- /cmd/cli/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/blushft/strana/controller" 9 | "github.com/blushft/strana/platform/config" 10 | "github.com/blushft/strana/platform/logger" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var rootCmd = &cobra.Command{ 16 | Use: "strana", 17 | Short: "Your way analytics", 18 | RunE: runServer, 19 | } 20 | 21 | func init() { 22 | viper.SetDefault("config", "./config.yaml") 23 | 24 | viper.SetEnvPrefix("strana") 25 | viper.AutomaticEnv() 26 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 27 | 28 | flgs := rootCmd.PersistentFlags() 29 | 30 | flgs.StringP("config", "c", "./config.yaml", "Config file") 31 | flgs.Bool("debug", false, "Enable debugging") 32 | flgs.StringP("server.host", "l", "", "Server listener host") 33 | flgs.StringP("server.port", "p", "8863", "Server listening port") 34 | 35 | if err := viper.BindPFlags(flgs); err != nil { 36 | panic(err) 37 | } 38 | 39 | cobra.OnInitialize(config.Init) 40 | 41 | rootCmd.AddCommand(configCommand()) 42 | } 43 | 44 | func configure() (*config.Config, error) { 45 | logger.Log().Info("getting configuration") 46 | 47 | v := viper.GetViper() 48 | cf := viper.GetString("config") 49 | 50 | viper.SetConfigFile(cf) 51 | 52 | if err := viper.ReadInConfig(); err != nil { 53 | var perr *os.PathError 54 | if !errors.As(err, &perr) { 55 | return nil, err 56 | } 57 | } 58 | 59 | conf, err := config.NewConfig(v) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return conf, nil 65 | } 66 | 67 | func Execute() error { 68 | return rootCmd.Execute() 69 | } 70 | 71 | func runServer(cmd *cobra.Command, args []string) error { 72 | logger.Log().Info("starting strana...") 73 | conf, err := configure() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | app, err := controller.New(*conf) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return app.Start() 84 | } 85 | -------------------------------------------------------------------------------- /cmd/cli/cli/config.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/davecgh/go-spew/spew" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func configCommand() *cobra.Command { 9 | return &cobra.Command{ 10 | Use: "config", 11 | Short: "Print Strana Configuration", 12 | RunE: func(cmd *cobra.Command, args []string) error { 13 | conf, err := configure() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | spew.Dump(conf) 19 | 20 | return nil 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | // Initialize processor constructors 7 | _ "github.com/blushft/strana/modules/init" 8 | _ "github.com/blushft/strana/processor/init" 9 | 10 | _ "github.com/joho/godotenv/autoload" 11 | 12 | "github.com/blushft/strana/cmd/cli/cli" 13 | ) 14 | 15 | func main() { 16 | if err := cli.Execute(); err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /controller/api.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gofiber/fiber" 7 | ) 8 | 9 | func (a *App) routes(rtr fiber.Router) error { 10 | api := rtr.Group("/api") 11 | cg := api.Group("/controller") 12 | 13 | cg.Get("/config", func(ctx *fiber.Ctx) { 14 | if err := ctx.JSON(a.conf); err != nil { 15 | ctx.Status(http.StatusInternalServerError).Send(err) 16 | } 17 | }) 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /controller/app.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/blushft/strana/platform/bus" 5 | "github.com/blushft/strana/platform/config" 6 | "github.com/blushft/strana/platform/logger" 7 | "github.com/blushft/strana/platform/server" 8 | "github.com/blushft/strana/platform/store" 9 | "github.com/oklog/run" 10 | ) 11 | 12 | type App struct { 13 | conf config.Config 14 | svr *server.Server 15 | bus bus.Bus 16 | store *store.SQLStore 17 | log *logger.Logger 18 | 19 | modules map[string]*container 20 | } 21 | 22 | func New(conf config.Config) (*App, error) { 23 | logger.Init(conf.Logger) 24 | 25 | l := logger.New().WithFields(logger.Fields{"app": "strana"}) 26 | 27 | s, err := store.NewSQL(conf.Database) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | svr := server.New(conf.Server, conf.Debug) 33 | 34 | bus, err := bus.New(conf.Bus) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | mods, err := newContainers(conf, svr, s, l) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | a := &App{ 45 | conf: conf, 46 | svr: svr, 47 | bus: bus, 48 | store: s, 49 | log: l, 50 | modules: mods, 51 | } 52 | 53 | svr.Mount(a.routes) 54 | 55 | return a, nil 56 | } 57 | 58 | func (a *App) Start() error { 59 | grp := run.Group{} 60 | 61 | grp.Add( 62 | a.svr.Start, 63 | func(e error) { 64 | a.log.Info("server stopping") 65 | a.svr.Shutdown() 66 | }, 67 | ) 68 | 69 | grp.Add( 70 | a.bus.Start, 71 | func(e error) { 72 | a.log.Info("bus stopping") 73 | if err := a.bus.Shutdown(); err != nil { 74 | a.log.WithError(err).Error("bus shutdown") 75 | } 76 | }, 77 | ) 78 | 79 | go func() { 80 | for k, c := range a.modules { 81 | a.log.Infof("mounting events for module %s", k) 82 | 83 | if err := a.bus.Mount(c.module()); err != nil { 84 | logger.Log().Fatal(err.Error()) 85 | } 86 | } 87 | }() 88 | 89 | return grp.Run() 90 | } 91 | -------------------------------------------------------------------------------- /controller/bus.go: -------------------------------------------------------------------------------- 1 | package controller 2 | -------------------------------------------------------------------------------- /controller/container.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/blushft/strana" 5 | "github.com/blushft/strana/modules" 6 | "github.com/blushft/strana/platform/bus" 7 | "github.com/blushft/strana/platform/config" 8 | "github.com/blushft/strana/platform/logger" 9 | "github.com/blushft/strana/platform/server" 10 | "github.com/blushft/strana/platform/store" 11 | ) 12 | 13 | type container struct { 14 | svr *server.Server 15 | bus bus.Bus 16 | store *store.SQLStore 17 | log *logger.Logger 18 | 19 | modc config.Module 20 | mod strana.Module 21 | } 22 | 23 | func newContainers(conf config.Config, svr *server.Server, store *store.SQLStore, l *logger.Logger) (map[string]*container, error) { 24 | cs := make(map[string]*container, len(conf.Modules)) 25 | 26 | for _, m := range conf.Modules { 27 | nc := &container{ 28 | svr: svr, 29 | store: store, 30 | log: l, 31 | modc: m, 32 | } 33 | 34 | if err := nc.initModule(); err != nil { 35 | return nil, err 36 | } 37 | 38 | cs[m.Name] = nc 39 | } 40 | 41 | return cs, nil 42 | } 43 | 44 | func (c *container) initModule() error { 45 | mod, err := modules.New(c.modc) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | c.log.Mount(mod.Logger) 51 | 52 | if err := c.store.Mount(mod.Services); err != nil { 53 | return err 54 | } 55 | 56 | if err := c.svr.Mount(mod.Routes); err != nil { 57 | return err 58 | } 59 | 60 | c.mod = mod 61 | 62 | return nil 63 | } 64 | 65 | func (c *container) start(b bus.Bus) error { 66 | if err := b.Mount(c.mod); err != nil { 67 | return err 68 | } 69 | 70 | c.bus = b 71 | 72 | return nil 73 | } 74 | 75 | func (c *container) name() string { 76 | return c.modc.Name 77 | } 78 | 79 | func (c *container) module() strana.Module { 80 | return c.mod 81 | } 82 | 83 | func (c *container) t() string { 84 | return c.modc.Type 85 | } 86 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | strana: 4 | image: blushft/strana 5 | build: 6 | context: . 7 | dockerfile: ./.docker/Dockerfile 8 | environment: 9 | - STRANA_DATABASE_DATABASE=/app/data/strana.db 10 | volumes: 11 | - "./.docker/data:/app/data" 12 | ports: 13 | - 8863:8863 14 | - 4442:4442 15 | - 4443:4443 16 | metabase: 17 | image: metabase/metabase 18 | volumes: 19 | - "./.docker/data:/data" 20 | ports: 21 | - 3000:3000 22 | -------------------------------------------------------------------------------- /event/context.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/fatih/structs" 9 | ) 10 | 11 | type ContextType string 12 | 13 | const ( 14 | ContextInvalid ContextType = "invalid" 15 | ) 16 | 17 | func GetContextType(typ string) ContextType { 18 | for k := range ctxreg { 19 | if strings.EqualFold(typ, string(k)) { 20 | return k 21 | } 22 | } 23 | 24 | return ContextInvalid 25 | } 26 | 27 | type Context interface { 28 | Type() ContextType 29 | Values() map[string]interface{} 30 | Interface() interface{} 31 | Validate() bool 32 | } 33 | 34 | type context struct { 35 | typ ContextType 36 | v interface{} 37 | } 38 | 39 | func newContext(typ ContextType, v interface{}) Context { 40 | return &context{ 41 | typ: typ, 42 | v: v, 43 | } 44 | } 45 | 46 | func (ctx context) Type() ContextType { 47 | return ctx.typ 48 | } 49 | 50 | func (ctx context) Values() map[string]interface{} { 51 | return structs.Map(ctx.v) 52 | } 53 | 54 | func (ctx *context) Interface() interface{} { 55 | return ctx.v 56 | } 57 | 58 | func (ctx *context) Validate() bool { 59 | return true 60 | } 61 | 62 | func emptyContext(typ ContextType) (Context, error) { 63 | ctor, ok := ctxreg[typ] 64 | if !ok { 65 | return nil, errors.New("context type unknown") 66 | } 67 | 68 | return ctor(), nil 69 | } 70 | 71 | func decodeContext(typ string, vals json.RawMessage) (Context, error) { 72 | ct := GetContextType(typ) 73 | if ct == ContextInvalid { 74 | return nil, errors.New("context type invalid") 75 | } 76 | 77 | ec, err := emptyContext(ct) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | nv := ec.Interface() 83 | if err := json.Unmarshal(vals, &nv); err != nil { 84 | return nil, err 85 | } 86 | 87 | ctx, ok := nv.(Context) 88 | if !ok { 89 | return nil, errors.New("invalid context") 90 | } 91 | return ctx, nil 92 | } 93 | -------------------------------------------------------------------------------- /event/contexts/action.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | "github.com/mitchellh/mapstructure" 7 | ) 8 | 9 | const ContextAction event.ContextType = "action" 10 | 11 | type Action struct { 12 | Category string `json:"category" structs:"category" mapstructure:"category"` 13 | Action string `json:"action" structs:"action" mapstructure:"action"` 14 | Label string `json:"label,omitempty" structs:"label,omitempty" mapstructure:"label,omitempty"` 15 | Property string `json:"property,omitempty" structs:"property,omitempty" mapstructure:"property,omitempty"` 16 | Value interface{} `json:"value,omitempty" structs:"value,omitempty" mapstructure:"value,omitempty"` 17 | } 18 | 19 | func newAction() event.Context { 20 | return &Action{} 21 | } 22 | 23 | func NewAction(cat, action string) event.Context { 24 | return &Action{ 25 | Category: cat, 26 | Action: action, 27 | } 28 | } 29 | 30 | func (ctx *Action) Type() event.ContextType { 31 | return ContextAction 32 | } 33 | 34 | func (ctx *Action) Values() map[string]interface{} { 35 | return structs.Map(ctx) 36 | } 37 | 38 | func (ctx *Action) Interface() interface{} { 39 | return ctx 40 | } 41 | 42 | func (ctx *Action) Validate() bool { 43 | if len(ctx.Category) == 0 || len(ctx.Action) == 0 { 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | func decodeAction(v interface{}) (event.Context, error) { 51 | var ctx Action 52 | if err := mapstructure.Decode(v, &ctx); err != nil { 53 | return nil, err 54 | } 55 | 56 | return &ctx, nil 57 | } 58 | -------------------------------------------------------------------------------- /event/contexts/alias.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextAlias event.ContextType = "alias" 9 | 10 | type Alias struct { 11 | From string 12 | To string 13 | } 14 | 15 | func NewAlias(from, to string) event.Context { 16 | return &Alias{ 17 | From: from, 18 | To: to, 19 | } 20 | } 21 | 22 | func (ctx *Alias) Type() event.ContextType { 23 | return ContextAlias 24 | } 25 | 26 | func (ctx *Alias) Values() map[string]interface{} { 27 | return structs.Map(ctx) 28 | } 29 | 30 | func (ctx *Alias) Interface() interface{} { 31 | return ctx 32 | } 33 | 34 | func (ctx *Alias) Validate() bool { 35 | if len(ctx.From) == 0 || len(ctx.To) == 0 { 36 | return false 37 | } 38 | 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /event/contexts/app.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextApp event.ContextType = "app" 9 | 10 | type App struct { 11 | Name string `json:"name" structs:"name" mapstructure:"name"` 12 | Version string `json:"version,omitempty" structs:"version,omitempty" mapstructure:"version,omitempty"` 13 | Build string `json:"build,omitempty" structs:"build,omitempty" mapstructure:"build,omitempty"` 14 | Namespace string `json:"namespace,omitempty" structs:"namespace,omitempty" mapstructure:"namespace,omitempty"` 15 | Properties map[string]interface{} `json:"properties,omitempty" structs:"properties,omitempty" mapstructure:"properties,omitempty"` 16 | } 17 | 18 | func NewApp(n string) event.Context { 19 | return &App{ 20 | Name: n, 21 | } 22 | } 23 | 24 | func (ctx *App) Type() event.ContextType { 25 | return ContextApp 26 | } 27 | 28 | func (ctx *App) Values() map[string]interface{} { 29 | return structs.Map(ctx) 30 | } 31 | 32 | func (ctx *App) Interface() interface{} { 33 | return ctx 34 | } 35 | 36 | func (ctx *App) Validate() bool { 37 | if len(ctx.Name) == 0 { 38 | return false 39 | } 40 | 41 | return true 42 | } 43 | -------------------------------------------------------------------------------- /event/contexts/browser.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextBrowser event.ContextType = "browser" 9 | 10 | type Browser struct { 11 | Name string `json:"name,omitempty" structs:"name,omitempty" mapstructure:"name,omitempty"` 12 | Version string `json:"version,omitempty" structs:"version,omitempty" mapstructure:"version,omitempty"` 13 | UserAgent string `json:"userAgent,omitempty" structs:"userAgent,omitempty" mapstructure:"userAgent,omitempty"` 14 | } 15 | 16 | func NewBrowser(n string) event.Context { 17 | return &Browser{ 18 | Name: n, 19 | } 20 | } 21 | 22 | func (ctx *Browser) Type() event.ContextType { 23 | return ContextBrowser 24 | } 25 | 26 | func (ctx *Browser) Values() map[string]interface{} { 27 | return structs.Map(ctx) 28 | } 29 | 30 | func (ctx *Browser) Interface() interface{} { 31 | return ctx 32 | } 33 | 34 | func (ctx *Browser) Validate() bool { 35 | if len(ctx.Name) == 0 { 36 | return false 37 | } 38 | 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /event/contexts/campaign.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextCampaign event.ContextType = "campaign" 9 | 10 | type Campaign struct { 11 | Name string `json:"name" structs:"name" mapstructure:"name"` 12 | Source string `json:"source,omitempty" structs:"source,omitempty" mapstructure:"source,omitempty"` 13 | Medium string `json:"medium,omitempty" structs:"medium,omitempty" mapstructure:"medium,omitempty"` 14 | Term string `json:"term,omitempty" structs:"term,omitempty" mapstructure:"term,omitempty"` 15 | Content string `json:"content,omitempty" structs:"content,omitempty" mapstructure:"content,omitempty"` 16 | } 17 | 18 | func (ctx *Campaign) Type() event.ContextType { 19 | return ContextCampaign 20 | } 21 | 22 | func (ctx *Campaign) Values() map[string]interface{} { 23 | return structs.Map(ctx) 24 | } 25 | 26 | func (ctx *Campaign) Interface() interface{} { 27 | return ctx 28 | } 29 | 30 | func (ctx *Campaign) Validate() bool { 31 | if len(ctx.Name) == 0 { 32 | return false 33 | } 34 | 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /event/contexts/connectivity.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextConnectivity event.ContextType = "connectivity" 9 | 10 | type Connectivity struct { 11 | Bluetooth bool `json:"bluetooth,omitempty" structs:"bluetooth,omitempty" mapstructure:"bluetooth,omitempty"` 12 | Cellular bool `json:"cellular,omitempty" structs:"cellular,omitempty" mapstructure:"cellular,omitempty"` 13 | WIFI bool `json:"wifi,omitempty" structs:"wifi,omitempty" mapstructure:"wifi,omitempty"` 14 | Ethernet bool `json:"ethernet,omitempty" structs:"ethernet,omitempty" mapstructure:"ethernet,omitempty"` 15 | Carrier string `json:"carrier,omitempty" structs:"carrier,omitempty" mapstructure:"carrier,omitempty"` 16 | ISP string `json:"isp,omitempty" structs:"isp,omitempty" mapstructure:"isp,omitempty"` 17 | } 18 | 19 | func (ctx *Connectivity) Type() event.ContextType { 20 | return ContextConnectivity 21 | } 22 | 23 | func (ctx *Connectivity) Values() map[string]interface{} { 24 | return structs.Map(ctx) 25 | } 26 | 27 | func (ctx *Connectivity) Interface() interface{} { 28 | return ctx 29 | } 30 | 31 | func (ctx *Connectivity) Validate() bool { 32 | 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /event/contexts/device.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextDevice event.ContextType = "device" 9 | 10 | type Device struct { 11 | ID string `json:"id,omitempty" structs:"id,omitempty" mapstructure:"id,omitempty"` 12 | Manufacturer string `json:"manufacturer,omitempty" structs:"manufacturer,omitempty" mapstructure:"manufacturer,omitempty"` 13 | Model string `json:"model,omitempty" structs:"model,omitempty" mapstructure:"model,omitempty"` 14 | Name string `json:"name,omitempty" structs:"name,omitempty" mapstructure:"name,omitempty"` 15 | Kind string `json:"type,omitempty" structs:"type,omitempty" mapstructure:"type,omitempty"` 16 | Version string `json:"version,omitempty" structs:"version,omitempty" mapstructure:"version,omitempty"` 17 | Mobile bool `json:"mobile,omitempty" structs:"mobile,omitempty" mapstructure:"mobile,omitempty"` 18 | Tablet bool `json:"tablet,omitempty" structs:"tablet,omitempty" mapstructure:"tablet,omitempty"` 19 | Desktop bool `json:"desktop,omitempty" structs:"desktop,omitempty" mapstructure:"desktop,omitempty"` 20 | Properties map[string]interface{} `json:"properties,omitempty" structs:"properties,omitempty" mapstructure:"properties,omitempty"` 21 | } 22 | 23 | func NewDeviceContext(id string) event.Context { 24 | return &Device{ 25 | ID: id, 26 | } 27 | } 28 | 29 | func (ctx *Device) Type() event.ContextType { 30 | return ContextDevice 31 | } 32 | 33 | func (ctx *Device) Values() map[string]interface{} { 34 | return structs.Map(ctx) 35 | } 36 | 37 | func (ctx *Device) Interface() interface{} { 38 | return ctx 39 | } 40 | 41 | func (ctx *Device) Validate() bool { 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /event/contexts/extra.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | ) 6 | 7 | const ContextExtra event.ContextType = "extra" 8 | 9 | type Extra map[string]interface{} 10 | 11 | func (ctx *Extra) Type() event.ContextType { 12 | return ContextExtra 13 | } 14 | 15 | func (ctx *Extra) Values() map[string]interface{} { 16 | return *ctx 17 | } 18 | 19 | func (ctx *Extra) Interface() interface{} { 20 | return ctx 21 | } 22 | 23 | func (ctx *Extra) Validate() bool { 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /event/contexts/group.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextGroup event.ContextType = "group" 9 | 10 | type Group struct { 11 | ID string `json:"id,omitempty" structs:"id,omitempty" mapstructure:"id,omitempty"` 12 | Name string `json:"name,omitempty" structs:"name,omitempty" mapstructure:"name,omitempty"` 13 | } 14 | 15 | func (ctx *Group) Type() event.ContextType { 16 | return ContextGroup 17 | } 18 | 19 | func (ctx *Group) Values() map[string]interface{} { 20 | return structs.Map(ctx) 21 | } 22 | 23 | func (ctx *Group) Interface() interface{} { 24 | return ctx 25 | } 26 | 27 | func (ctx *Group) Validate() bool { 28 | if len(ctx.ID) == 0 || len(ctx.Name) == 0 { 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /event/contexts/library.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextLibrary event.ContextType = "library" 9 | 10 | type Library struct { 11 | Name string `json:"name" structs:"name" mapstructure:"name"` 12 | Version string `json:"version,omitempty" structs:"version,omitempty" mapstructure:"version,omitempty"` 13 | } 14 | 15 | func (ctx *Library) Type() event.ContextType { 16 | return ContextLibrary 17 | } 18 | 19 | func (ctx *Library) Values() map[string]interface{} { 20 | return structs.Map(ctx) 21 | } 22 | 23 | func (ctx *Library) Interface() interface{} { 24 | return ctx 25 | } 26 | 27 | func (ctx *Library) Validate() bool { 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /event/contexts/location.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextLocation event.ContextType = "location" 9 | 10 | type Location struct { 11 | Street string `json:"street,omitempty" structs:"street,omitempty" mapstructure:"street,omitempty"` 12 | City string `json:"city,omitempty" structs:"city,omitempty" mapstructure:"city,omitempty"` 13 | State string `json:"state,omitempty" structs:"state,omitempty" mapstructure:"state,omitempty"` 14 | PostalCode string `json:"postalCode,omitempty" structs:"postalCode,omitempty" mapstructure:"postalCode,omitempty"` 15 | Region string `json:"region,omitempty" structs:"region,omitempty" mapstructure:"region,omitempty"` 16 | Locale string `json:"locale,omitempty" structs:"locale,omitempty" mapstructure:"locale,omitempty"` 17 | Country string `json:"country,omitempty" structs:"country,omitempty" mapstructure:"country,omitempty"` 18 | Longitude float64 `json:"longitude,omitempty" structs:"longitude,omitempty" mapstructure:"longitude,omitempty"` 19 | Latitude float64 `json:"latitude,omitempty" structs:"latitude,omitempty" mapstructure:"latitude,omitempty"` 20 | Timezone string `json:"timezone,omitempty" structs:"timezone,omitempty" mapstructure:"timezone,omitempty"` 21 | } 22 | 23 | func (ctx *Location) Type() event.ContextType { 24 | return ContextLocation 25 | } 26 | 27 | func (ctx *Location) Values() map[string]interface{} { 28 | return structs.Map(ctx) 29 | } 30 | 31 | func (ctx *Location) Interface() interface{} { 32 | return ctx 33 | } 34 | 35 | func (ctx *Location) Validate() bool { 36 | return true 37 | } 38 | -------------------------------------------------------------------------------- /event/contexts/network.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | 7 | "github.com/blushft/strana/event" 8 | "github.com/fatih/structs" 9 | ) 10 | 11 | const ContextNetwork event.ContextType = "network" 12 | 13 | type Network struct { 14 | IP net.IP `json:"ip,omitempty" structs:"ip,omitempty" mapstructure:"ip,omitempty"` 15 | UserAgent string `json:"userAgent,omitempty" structs:"userAgent,omitempty" mapstructure:"userAgent,omitempty"` 16 | } 17 | 18 | func NewNetwork(ip string) *Network { 19 | nip := net.ParseIP(ip) 20 | 21 | return &Network{ 22 | IP: nip, 23 | } 24 | } 25 | 26 | func (ctx *Network) Type() event.ContextType { 27 | return ContextNetwork 28 | } 29 | 30 | func (ctx *Network) Values() map[string]interface{} { 31 | return structs.Map(ctx) 32 | } 33 | 34 | func (ctx *Network) Interface() interface{} { 35 | return ctx 36 | } 37 | 38 | func (ctx *Network) Validate() bool { 39 | if len(ctx.IP.String()) == 0 { 40 | return false 41 | } 42 | 43 | return true 44 | } 45 | 46 | func (ctx *Network) MarshalJSON() ([]byte, error) { 47 | ipt, err := ctx.IP.MarshalText() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | m := map[string]interface{}{ 53 | "ip": string(ipt), 54 | "userAgent": ctx.UserAgent, 55 | } 56 | 57 | return json.Marshal(m) 58 | } 59 | 60 | func (ctx *Network) UnmarshalJSON(b []byte) error { 61 | m := map[string]interface{}{} 62 | if err := json.Unmarshal(b, &m); err != nil { 63 | return err 64 | } 65 | 66 | ua, ok := m["userAgent"] 67 | if ok { 68 | ctx.UserAgent = ua.(string) 69 | } 70 | 71 | ipt, ok := m["ip"] 72 | if ok { 73 | ipb := ipt.(string) 74 | ip := net.IP{} 75 | if err := ip.UnmarshalText([]byte(ipb)); err != nil { 76 | return err 77 | } 78 | 79 | ctx.IP = ip 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /event/contexts/os.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextOS event.ContextType = "os" 9 | 10 | type OS struct { 11 | Name string `json:"name" structs:"name" mapstructure:"name"` 12 | Family string `json:"family,omitempty" structs:"family,omitempty" mapstructure:"family,omitempty"` 13 | Platform string `json:"platform,omitempty" structs:"platform,omitempty" mapstructure:"platform,omitempty"` 14 | Version string `json:"version,omitempty" structs:"version,omitempty" mapstructure:"version,omitempty"` 15 | } 16 | 17 | func NewOSContext(name string) event.Context { 18 | return &OS{ 19 | Name: name, 20 | } 21 | } 22 | 23 | func (ctx *OS) Type() event.ContextType { 24 | return ContextOS 25 | } 26 | 27 | func (ctx *OS) Values() map[string]interface{} { 28 | return structs.Map(ctx) 29 | } 30 | 31 | func (ctx *OS) Interface() interface{} { 32 | return ctx 33 | } 34 | 35 | func (ctx *OS) Validate() bool { 36 | if len(ctx.Name) == 0 { 37 | return false 38 | } 39 | 40 | return true 41 | } 42 | -------------------------------------------------------------------------------- /event/contexts/page.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextPage event.ContextType = "page" 9 | 10 | type Page struct { 11 | Hash string `json:"hash,omitempty" structs:"hash,omitempty" mapstructure:"hash,omitempty"` 12 | Path string `json:"path,omitempty" structs:"path,omitempty" mapstructure:"path,omitempty"` 13 | Referrer string `json:"referrer,omitempty" structs:"referrer,omitempty" mapstructure:"referrer,omitempty"` 14 | Search string `json:"search,omitempty" structs:"search,omitempty" mapstructure:"search,omitempty"` 15 | Title string `json:"title,omitempty" structs:"title,omitempty" mapstructure:"title,omitempty"` 16 | Hostname string `json:"hostname" structs:"hostname" mapstructure:"hostname"` 17 | } 18 | 19 | func (ctx *Page) Type() event.ContextType { 20 | return ContextPage 21 | } 22 | 23 | func (ctx *Page) Values() map[string]interface{} { 24 | return structs.Map(ctx) 25 | } 26 | 27 | func (ctx *Page) Interface() interface{} { 28 | return ctx 29 | } 30 | 31 | func (ctx *Page) Validate() bool { 32 | if len(ctx.Hostname) == 0 { 33 | return false 34 | } 35 | 36 | return true 37 | } 38 | -------------------------------------------------------------------------------- /event/contexts/referrer.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextReferrer event.ContextType = "referrer" 9 | 10 | type Referrer struct { 11 | RefType string `json:"type,omitempty" structs:"type,omitempty" mapstructure:"type,omitempty"` 12 | Name string `json:"name,omitempty" structs:"name,omitempty" mapstructure:"name,omitempty"` 13 | Hostname string `json:"hostname,omitempty" structs:"hostname,omitempty" mapstructure:"hostname,omitempty"` 14 | Link string `json:"link,omitempty" structs:"link,omitempty" mapstructure:"link,omitempty"` 15 | } 16 | 17 | func (ctx *Referrer) Type() event.ContextType { 18 | return ContextReferrer 19 | } 20 | 21 | func (ctx *Referrer) Values() map[string]interface{} { 22 | return structs.Map(ctx) 23 | } 24 | 25 | func (ctx *Referrer) Interface() interface{} { 26 | return ctx 27 | } 28 | 29 | func (ctx *Referrer) Validate() bool { 30 | if len(ctx.Hostname) == 0 { 31 | return false 32 | } 33 | 34 | return true 35 | } 36 | -------------------------------------------------------------------------------- /event/contexts/screen.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextScreen event.ContextType = "screen" 9 | 10 | type Screen struct { 11 | Name string `json:"name" structs:"name" mapstructure:"name"` 12 | Category string `json:"category,omitempty" structs:"category,omitempty" mapstructure:"category,omitempty"` 13 | } 14 | 15 | func (ctx *Screen) Type() event.ContextType { 16 | return ContextScreen 17 | } 18 | 19 | func (ctx *Screen) Values() map[string]interface{} { 20 | return structs.Map(ctx) 21 | } 22 | 23 | func (ctx *Screen) Interface() interface{} { 24 | return ctx 25 | } 26 | 27 | func (ctx *Screen) Validate() bool { 28 | if len(ctx.Name) == 0 { 29 | return false 30 | } 31 | 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /event/contexts/session.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextSession event.ContextType = "session" 9 | 10 | type Session struct { 11 | ID string `json:"id" structs:"id" mapstructure:"id"` 12 | } 13 | 14 | func (ctx *Session) Type() event.ContextType { 15 | return ContextSession 16 | } 17 | 18 | func (ctx *Session) Values() map[string]interface{} { 19 | return structs.Map(ctx) 20 | } 21 | 22 | func (ctx *Session) Interface() interface{} { 23 | return ctx 24 | } 25 | 26 | func (ctx *Session) Validate() bool { 27 | if len(ctx.ID) == 0 { 28 | return false 29 | } 30 | 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /event/contexts/timing.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextTiming event.ContextType = "timing" 9 | 10 | type Timing struct { 11 | Category string `json:"category,omitempty" structs:"category,omitempty" mapstructure:"category,omitempty"` 12 | Label string `json:"label,omitempty" structs:"label,omitempty" mapstructure:"label,omitempty"` 13 | Unit string `json:"unit,omitempty" structs:"unit,omitempty" mapstructure:"unit,omitempty"` 14 | Variable string `json:"variable,omitempty" structs:"variable,omitempty" mapstructure:"variable,omitempty"` 15 | Value float64 `json:"value,omitempty" structs:"value,omitempty" mapstructure:"value,omitempty"` 16 | } 17 | 18 | func (ctx *Timing) Type() event.ContextType { 19 | return ContextTiming 20 | } 21 | 22 | func (ctx *Timing) Values() map[string]interface{} { 23 | return structs.Map(ctx) 24 | } 25 | 26 | func (ctx *Timing) Interface() interface{} { 27 | return ctx 28 | } 29 | 30 | func (ctx *Timing) Validate() bool { 31 | if ctx.Value == 0 { 32 | return false 33 | } 34 | 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /event/contexts/viewport.go: -------------------------------------------------------------------------------- 1 | package contexts 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/fatih/structs" 6 | ) 7 | 8 | const ContextViewport event.ContextType = "viewport" 9 | 10 | type Viewport struct { 11 | Density int `json:"density,omitempty" structs:"density,omitempty" mapstructure:"density,omitempty"` 12 | Width int `json:"width,omitempty" structs:"width,omitempty" mapstructure:"width,omitempty"` 13 | Height int `json:"height,omitempty" structs:"height,omitempty" mapstructure:"height,omitempty"` 14 | } 15 | 16 | func (ctx *Viewport) Type() event.ContextType { 17 | return ContextViewport 18 | } 19 | 20 | func (ctx *Viewport) Values() map[string]interface{} { 21 | return structs.Map(ctx) 22 | } 23 | 24 | func (ctx *Viewport) Interface() interface{} { 25 | return ctx 26 | } 27 | 28 | func (ctx *Viewport) Validate() bool { 29 | 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /event/events/action.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeAction event.Type = "action" 9 | 10 | func Action(cat, action string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(contexts.NewAction(cat, action)), 14 | event.WithValidator(actionValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypeAction, eopts...) 20 | } 21 | 22 | func actionValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_action", event.ContextValid(contexts.ContextAction)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/alias.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeAlias event.Type = "alias" 9 | 10 | func Alias(from, to, user string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.User{UserID: user}), 14 | event.WithContext(&contexts.Alias{From: from, To: to}), 15 | event.WithValidator(aliasValidator()), 16 | }, 17 | opts, 18 | ) 19 | 20 | return event.New(EventTypeAlias, eopts...) 21 | } 22 | 23 | func aliasValidator() event.Validator { 24 | return event.NewValidator( 25 | event.WithRule("valid_group", event.ContextValid(contexts.ContextAlias)), 26 | event.WithRule("valid_user", event.ContextValid(contexts.ContextUser)), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /event/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/blushft/strana/event" 4 | 5 | func init() { 6 | event.RegisterType(EventTypeAction) 7 | event.RegisterType(EventTypeAlias) 8 | event.RegisterType(EventTypeGroup) 9 | event.RegisterType(EventTypeIdentify) 10 | event.RegisterType(EventTypePageview) 11 | event.RegisterType(EventTypeScreen) 12 | event.RegisterType(EventTypeSession) 13 | event.RegisterType(EventTypeTiming) 14 | } 15 | 16 | func mergeOptions(opts ...[]event.Option) []event.Option { 17 | var mopts []event.Option 18 | 19 | for _, os := range opts { 20 | mopts = append(mopts, os...) 21 | } 22 | 23 | return mopts 24 | } 25 | -------------------------------------------------------------------------------- /event/events/group.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeGroup event.Type = "group" 9 | 10 | func Group(name string, user string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.User{UserID: user}), 14 | event.WithContext(&contexts.Group{Name: name}), 15 | event.WithValidator(groupValidator()), 16 | }, 17 | opts, 18 | ) 19 | 20 | return event.New(EventTypeAction, eopts...) 21 | } 22 | 23 | func groupValidator() event.Validator { 24 | return event.NewValidator( 25 | event.WithRule("group_context", event.HasContext(contexts.ContextGroup)), 26 | event.WithRule("user_context", event.HasContext(contexts.ContextUser)), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /event/events/identify.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeIdentify event.Type = "identify" 9 | 10 | func Identify(user string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.User{UserID: user}), 14 | event.WithValidator(identifyValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypeIdentify, eopts...) 20 | } 21 | 22 | func identifyValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_user", event.ContextValid(contexts.ContextUser)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/pageview.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypePageview event.Type = "pageview" 9 | 10 | func Pageview(hn string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.Page{Hostname: hn}), 14 | event.WithValidator(pageviewValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypePageview, eopts...) 20 | } 21 | 22 | func pageviewValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_page", event.ContextValid(contexts.ContextPage)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/screen.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeScreen event.Type = "screen" 9 | 10 | func Screen(name string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.Screen{Name: name}), 14 | event.WithValidator(screenValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypeScreen, eopts...) 20 | } 21 | 22 | func screenValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_screen", event.ContextValid(contexts.ContextScreen)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/session.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeSession event.Type = "session" 9 | 10 | func Session(id string, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(&contexts.Session{ID: id}), 14 | event.WithValidator(sessionValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypeSession, eopts...) 20 | } 21 | 22 | func sessionValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_session", event.ContextValid(contexts.ContextSession)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/timing.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | const EventTypeTiming event.Type = "timing" 9 | 10 | func Timing(t *contexts.Timing, opts ...event.Option) *event.Event { 11 | eopts := mergeOptions( 12 | []event.Option{ 13 | event.WithContext(t), 14 | event.WithValidator(timingValidator()), 15 | }, 16 | opts, 17 | ) 18 | 19 | return event.New(EventTypeTiming, eopts...) 20 | } 21 | 22 | func timingValidator() event.Validator { 23 | return event.NewValidator( 24 | event.WithRule("valid_timing", event.ContextValid(contexts.ContextTiming)), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /event/events/transaction.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/blushft/strana/event" 4 | 5 | const EventTypeTransaction event.Type = "transaction" 6 | -------------------------------------------------------------------------------- /event/options.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type Option func(*Event) 4 | 5 | func TrackingID(id string) Option { 6 | return func(e *Event) { 7 | e.TrackingID = id 8 | } 9 | } 10 | 11 | func UserID(id string) Option { 12 | return func(e *Event) { 13 | e.UserID = id 14 | } 15 | } 16 | 17 | func GroupID(id string) Option { 18 | return func(e *Event) { 19 | e.GroupID = id 20 | } 21 | } 22 | 23 | func DeviceID(id string) Option { 24 | return func(e *Event) { 25 | e.DeviceID = id 26 | } 27 | } 28 | 29 | func SessionID(id string) Option { 30 | return func(e *Event) { 31 | e.SessionID = id 32 | } 33 | } 34 | 35 | func Anonymous(b bool) Option { 36 | return func(e *Event) { 37 | e.Anonymous = b 38 | } 39 | } 40 | 41 | func NonInteractive() Option { 42 | return func(e *Event) { 43 | e.NonInteractive = true 44 | } 45 | } 46 | 47 | func Interactive() Option { 48 | return func(e *Event) { 49 | e.NonInteractive = false 50 | } 51 | } 52 | 53 | func Platform(p string) Option { 54 | return func(e *Event) { 55 | e.Platform = p 56 | } 57 | } 58 | 59 | func Channel(c string) Option { 60 | return func(e *Event) { 61 | e.Channel = c 62 | } 63 | } 64 | 65 | func WithContext(ctx Context) Option { 66 | return func(e *Event) { 67 | e.SetContext(ctx) 68 | } 69 | } 70 | 71 | func WithContexts(ctx ...Context) Option { 72 | return func(e *Event) { 73 | e.SetContexts(ctx...) 74 | } 75 | } 76 | 77 | func WithValidator(v Validator) Option { 78 | return func(e *Event) { 79 | e.validator = v 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /event/registry.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | var evtreg EventRegistry 4 | var ctxreg ContextRegistry 5 | 6 | func init() { 7 | ctxreg = make(map[ContextType]ContextContructor) 8 | evtreg = make(map[Type]bool) 9 | } 10 | 11 | type EventConstructor func(...Option) *Event 12 | type EventRegistry map[Type]bool 13 | 14 | func RegisterType(typ Type) { 15 | evtreg[typ] = true 16 | } 17 | 18 | type ContextContructor func() Context 19 | type ContextRegistry map[ContextType]ContextContructor 20 | 21 | func RegisterContext(typ ContextType, ctor ContextContructor) { 22 | ctxreg[typ] = ctor 23 | } 24 | -------------------------------------------------------------------------------- /event/tests/events_test.go: -------------------------------------------------------------------------------- 1 | package events_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/event/contexts" 8 | "github.com/blushft/strana/event/events" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type EventSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func TestRunEventSuite(t *testing.T) { 17 | suite.Run(t, new(EventSuite)) 18 | } 19 | 20 | func (s *EventSuite) TestNewEvent() { 21 | evt := events.Action("downloads", "get_docs", 22 | event.TrackingID("abc123"), 23 | event.WithContext(&contexts.User{UserID: "my@guy.com"}), 24 | ) 25 | 26 | s.Require().True(evt.Validate()) 27 | 28 | s.Equal(2, len(evt.Context.List())) 29 | } 30 | 31 | func (s *EventSuite) TestContextIter() { 32 | evt := events.Group("testers", "my@guy.com", 33 | event.TrackingID("abc123"), 34 | event.DeviceID("some_hostname"), 35 | event.WithContext(contexts.NewNetwork("192.168.0.1")), 36 | ) 37 | 38 | s.Require().True(evt.Validate()) 39 | 40 | it := evt.Context.Iter() 41 | count := 0 42 | for ctx := it.First(); ctx != nil; ctx = it.Next() { 43 | s.T().Logf("visiting %v", ctx.Type()) 44 | count++ 45 | } 46 | 47 | s.Equal(3, count) 48 | } 49 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package strana 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blushft/strana 2 | 3 | go 1.14 4 | 5 | require ( 6 | contrib.go.opencensus.io/integrations/ocsql v0.1.6 7 | github.com/allegro/bigcache v1.2.1 8 | github.com/apex/log v1.9.0 9 | github.com/containous/yaegi v0.8.14 10 | github.com/davecgh/go-spew v1.1.1 11 | github.com/dgraph-io/ristretto v0.0.3 // indirect 12 | github.com/eko/gocache v1.0.0 13 | github.com/facebook/ent v0.4.1 14 | github.com/fatih/structs v1.1.0 15 | github.com/gofiber/fiber v1.14.4 16 | github.com/gofiber/websocket v0.5.1 17 | github.com/gofrs/uuid v3.3.0+incompatible // indirect 18 | github.com/google/uuid v1.1.2 19 | github.com/jackc/pgx/v4 v4.8.1 20 | github.com/joho/godotenv v1.3.0 21 | github.com/lib/pq v1.8.0 22 | github.com/mattn/go-sqlite3 v1.14.2 23 | github.com/micro/go-micro v1.18.0 24 | github.com/micro/go-micro/v2 v2.9.1 25 | github.com/mileusna/useragent v1.0.2 26 | github.com/minio/minio-go/v6 v6.0.57 27 | github.com/mitchellh/mapstructure v1.3.3 28 | github.com/nats-io/nats-server/v2 v2.1.7 29 | github.com/nats-io/nats.go v1.10.0 30 | github.com/nsqio/go-nsq v1.0.7 31 | github.com/nsqio/nsq v1.2.0 32 | github.com/oklog/run v1.1.0 33 | github.com/oschwald/geoip2-golang v1.4.0 34 | github.com/paulbellamy/ratecounter v0.2.0 35 | github.com/pkg/errors v0.9.1 36 | github.com/spaolacci/murmur3 v1.1.0 // indirect 37 | github.com/spf13/cobra v1.0.0 38 | github.com/spf13/viper v1.7.1 39 | github.com/stretchr/testify v1.6.1 40 | github.com/vmihailenco/msgpack v4.0.4+incompatible 41 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 // indirect 42 | gopkg.in/resty.v1 v1.12.0 43 | ) 44 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true 8 | } -------------------------------------------------------------------------------- /modules/init/init.go: -------------------------------------------------------------------------------- 1 | package init 2 | 3 | import ( 4 | //import to register module constructors 5 | _ "github.com/blushft/strana/modules/processor/enhancer" 6 | _ "github.com/blushft/strana/modules/processor/fanin" 7 | _ "github.com/blushft/strana/modules/processor/sink" 8 | _ "github.com/blushft/strana/modules/sink/loader" 9 | _ "github.com/blushft/strana/modules/sink/reporter" 10 | _ "github.com/blushft/strana/modules/source/collector" 11 | _ "github.com/blushft/strana/modules/source/webhook" 12 | ) 13 | -------------------------------------------------------------------------------- /modules/modules.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/blushft/strana" 8 | "github.com/blushft/strana/platform/config" 9 | "github.com/mitchellh/mapstructure" 10 | ) 11 | 12 | var _globalRegistry = ®istry{ctors: make(map[string]strana.ModuleConstructor)} 13 | 14 | func Register(name string, mod strana.ModuleConstructor) { 15 | _globalRegistry.mu.Lock() 16 | _globalRegistry.ctors[name] = mod 17 | _globalRegistry.mu.Unlock() 18 | } 19 | 20 | type registry struct { 21 | mu sync.Mutex 22 | ctors map[string]strana.ModuleConstructor 23 | } 24 | 25 | func (reg *registry) new(conf config.Module) (strana.Module, error) { 26 | reg.mu.Lock() 27 | defer reg.mu.Unlock() 28 | 29 | mod, ok := reg.ctors[conf.Type] 30 | if !ok { 31 | return nil, errors.New("no module found for " + conf.Type) 32 | } 33 | 34 | return mod(conf) 35 | } 36 | 37 | func New(conf config.Module) (strana.Module, error) { 38 | return _globalRegistry.new(conf) 39 | } 40 | 41 | func BindOptions(m map[string]interface{}, v interface{}) error { 42 | return mapstructure.Decode(m, v) 43 | } 44 | -------------------------------------------------------------------------------- /modules/processor/enhancer/paths.go: -------------------------------------------------------------------------------- 1 | package enhancer 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | func cleanPathname(p string) string { 9 | return "/" + strings.TrimLeft(p, "/") 10 | } 11 | 12 | func cleanHostname(h string) string { 13 | u, err := url.Parse(h) 14 | if err != nil { 15 | return "" 16 | } 17 | 18 | return u.Scheme + "://" + u.Host 19 | } 20 | -------------------------------------------------------------------------------- /modules/sink/loader/command/track_event.go: -------------------------------------------------------------------------------- 1 | package track 2 | -------------------------------------------------------------------------------- /modules/sink/loader/gen.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | //go:generate go run github.com/facebook/ent/cmd/entc generate ./schema --target ./store/ent 4 | -------------------------------------------------------------------------------- /modules/sink/loader/loader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "github.com/blushft/strana" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/modules" 8 | "github.com/blushft/strana/modules/sink/loader/entity" 9 | ls "github.com/blushft/strana/modules/sink/loader/store" 10 | "github.com/blushft/strana/platform/bus/message" 11 | "github.com/blushft/strana/platform/config" 12 | "github.com/blushft/strana/platform/logger" 13 | "github.com/blushft/strana/platform/store" 14 | "github.com/gofiber/fiber" 15 | ) 16 | 17 | func init() { 18 | modules.Register("loader", New) 19 | } 20 | 21 | type Loader interface { 22 | strana.Sink 23 | } 24 | 25 | type loader struct { 26 | conf config.Module 27 | 28 | log *logger.Logger 29 | pub strana.Publisher 30 | 31 | restore entity.RawEventManager 32 | } 33 | 34 | func New(conf config.Module) (strana.Module, error) { 35 | return &loader{ 36 | conf: conf, 37 | }, nil 38 | } 39 | 40 | func (l *loader) Routes(rtr fiber.Router) error { 41 | return nil 42 | } 43 | 44 | func (l *loader) Events(eh strana.EventHandler) error { 45 | l.pub = eh.Publisher() 46 | return eh.Subscriber().Subscribe(l.conf.Source, l.handle) 47 | } 48 | 49 | func (l *loader) Services(s *store.SQLStore) error { 50 | dbc, err := ls.New(s) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | l.restore = entity.NewRawEventService(dbc) 56 | 57 | return nil 58 | } 59 | 60 | func (l *loader) Logger(lg *logger.Logger) { 61 | l.log = lg.WithFields(logger.Fields{"module": "loader"}) 62 | } 63 | 64 | func (l *loader) Publish(evt *event.Event) error { 65 | return l.pub.Publish(l.conf.Source, message.NewMessage(evt)) 66 | } 67 | 68 | func (l *loader) handle(msg *message.Message) error { 69 | return l.storeMessage(msg.Event) 70 | } 71 | 72 | func (l *loader) storeMessage(evt *event.Event) error { 73 | re := &entity.RawEvent{Event: evt} 74 | 75 | return l.restore.Create(re) 76 | } 77 | -------------------------------------------------------------------------------- /modules/sink/loader/schema/rawevent.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/field" 6 | "github.com/facebook/ent/schema/index" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // RawEvent holds the schema definition for the RawEvent entity. 11 | type RawEvent struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the RawEvent. 16 | func (RawEvent) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.UUID("id", uuid.UUID{}).Default(uuid.New), 19 | field.UUID("tracking_id", uuid.UUID{}), 20 | field.String("user_id"), 21 | field.Bool("anonymous"), 22 | field.String("group_id").Optional(), 23 | field.String("session_id").Optional(), 24 | field.String("device_id").Optional(), 25 | field.String("event"), 26 | field.Bool("non_interactive"), 27 | field.String("channel").Optional(), 28 | field.String("platform").Optional(), 29 | field.Time("timestamp"), 30 | field.JSON("context", map[string]interface{}{}), 31 | } 32 | } 33 | 34 | // Edges of the RawEvent. 35 | func (RawEvent) Edges() []ent.Edge { 36 | return nil 37 | } 38 | 39 | func (RawEvent) Indexes() []ent.Index { 40 | return []ent.Index{ 41 | index.Fields("user_id", "tracking_id", "group_id", "session_id", "device_id", "event"), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/config.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "github.com/facebook/ent" 7 | "github.com/facebook/ent/dialect" 8 | ) 9 | 10 | // Option function to configure the client. 11 | type Option func(*config) 12 | 13 | // Config is the configuration for the client and its builder. 14 | type config struct { 15 | // driver used for executing database requests. 16 | driver dialect.Driver 17 | // debug enable a debug logging. 18 | debug bool 19 | // log used for logging on debug mode. 20 | log func(...interface{}) 21 | // hooks to execute on mutations. 22 | hooks *hooks 23 | } 24 | 25 | // hooks per client, for fast access. 26 | type hooks struct { 27 | RawEvent []ent.Hook 28 | } 29 | 30 | // Options applies the options on the config object. 31 | func (c *config) options(opts ...Option) { 32 | for _, opt := range opts { 33 | opt(c) 34 | } 35 | if c.debug { 36 | c.driver = dialect.Debug(c.driver, c.log) 37 | } 38 | } 39 | 40 | // Debug enables debug logging on the ent.Driver. 41 | func Debug() Option { 42 | return func(c *config) { 43 | c.debug = true 44 | } 45 | } 46 | 47 | // Log sets the logging function for debug mode. 48 | func Log(fn func(...interface{})) Option { 49 | return func(c *config) { 50 | c.log = fn 51 | } 52 | } 53 | 54 | // Driver configures the client driver. 55 | func Driver(driver dialect.Driver) Option { 56 | return func(c *config) { 57 | c.driver = driver 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type clientCtxKey struct{} 10 | 11 | // FromContext returns the Client stored in a context, or nil if there isn't one. 12 | func FromContext(ctx context.Context) *Client { 13 | c, _ := ctx.Value(clientCtxKey{}).(*Client) 14 | return c 15 | } 16 | 17 | // NewContext returns a new context with the given Client attached. 18 | func NewContext(parent context.Context, c *Client) context.Context { 19 | return context.WithValue(parent, clientCtxKey{}, c) 20 | } 21 | 22 | type txCtxKey struct{} 23 | 24 | // TxFromContext returns the Tx stored in a context, or nil if there isn't one. 25 | func TxFromContext(ctx context.Context) *Tx { 26 | tx, _ := ctx.Value(txCtxKey{}).(*Tx) 27 | return tx 28 | } 29 | 30 | // NewTxContext returns a new context with the given Client attached. 31 | func NewTxContext(parent context.Context, tx *Tx) context.Context { 32 | return context.WithValue(parent, txCtxKey{}, tx) 33 | } 34 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/enttest/enttest.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package enttest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/blushft/strana/modules/sink/loader/store/ent" 9 | // required by schema hooks. 10 | _ "github.com/blushft/strana/modules/sink/loader/store/ent/runtime" 11 | 12 | "github.com/facebook/ent/dialect/sql/schema" 13 | ) 14 | 15 | type ( 16 | // TestingT is the interface that is shared between 17 | // testing.T and testing.B and used by enttest. 18 | TestingT interface { 19 | FailNow() 20 | Error(...interface{}) 21 | } 22 | 23 | // Option configures client creation. 24 | Option func(*options) 25 | 26 | options struct { 27 | opts []ent.Option 28 | migrateOpts []schema.MigrateOption 29 | } 30 | ) 31 | 32 | // WithOptions forwards options to client creation. 33 | func WithOptions(opts ...ent.Option) Option { 34 | return func(o *options) { 35 | o.opts = append(o.opts, opts...) 36 | } 37 | } 38 | 39 | // WithMigrateOptions forwards options to auto migration. 40 | func WithMigrateOptions(opts ...schema.MigrateOption) Option { 41 | return func(o *options) { 42 | o.migrateOpts = append(o.migrateOpts, opts...) 43 | } 44 | } 45 | 46 | func newOptions(opts []Option) *options { 47 | o := &options{} 48 | for _, opt := range opts { 49 | opt(o) 50 | } 51 | return o 52 | } 53 | 54 | // Open calls ent.Open and auto-run migration. 55 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { 56 | o := newOptions(opts) 57 | c, err := ent.Open(driverName, dataSourceName, o.opts...) 58 | if err != nil { 59 | t.Error(err) 60 | t.FailNow() 61 | } 62 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 63 | t.Error(err) 64 | t.FailNow() 65 | } 66 | return c 67 | } 68 | 69 | // NewClient calls ent.NewClient and auto-run migration. 70 | func NewClient(t TestingT, opts ...Option) *ent.Client { 71 | o := newOptions(opts) 72 | c := ent.NewClient(o.opts...) 73 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 74 | t.Error(err) 75 | t.FailNow() 76 | } 77 | return c 78 | } 79 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/migrate/schema.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "github.com/facebook/ent/dialect/sql/schema" 7 | "github.com/facebook/ent/schema/field" 8 | ) 9 | 10 | var ( 11 | // RawEventsColumns holds the columns for the "raw_events" table. 12 | RawEventsColumns = []*schema.Column{ 13 | {Name: "id", Type: field.TypeUUID}, 14 | {Name: "tracking_id", Type: field.TypeUUID}, 15 | {Name: "user_id", Type: field.TypeString}, 16 | {Name: "anonymous", Type: field.TypeBool}, 17 | {Name: "group_id", Type: field.TypeString, Nullable: true}, 18 | {Name: "session_id", Type: field.TypeString, Nullable: true}, 19 | {Name: "device_id", Type: field.TypeString, Nullable: true}, 20 | {Name: "event", Type: field.TypeString}, 21 | {Name: "non_interactive", Type: field.TypeBool}, 22 | {Name: "channel", Type: field.TypeString, Nullable: true}, 23 | {Name: "platform", Type: field.TypeString, Nullable: true}, 24 | {Name: "timestamp", Type: field.TypeTime}, 25 | {Name: "context", Type: field.TypeJSON}, 26 | } 27 | // RawEventsTable holds the schema information for the "raw_events" table. 28 | RawEventsTable = &schema.Table{ 29 | Name: "raw_events", 30 | Columns: RawEventsColumns, 31 | PrimaryKey: []*schema.Column{RawEventsColumns[0]}, 32 | ForeignKeys: []*schema.ForeignKey{}, 33 | Indexes: []*schema.Index{ 34 | { 35 | Name: "rawevent_user_id_tracking_id_group_id_session_id_device_id_event", 36 | Unique: false, 37 | Columns: []*schema.Column{RawEventsColumns[2], RawEventsColumns[1], RawEventsColumns[4], RawEventsColumns[5], RawEventsColumns[6], RawEventsColumns[7]}, 38 | }, 39 | }, 40 | } 41 | // Tables holds all the tables in the schema. 42 | Tables = []*schema.Table{ 43 | RawEventsTable, 44 | } 45 | ) 46 | 47 | func init() { 48 | } 49 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package predicate 4 | 5 | import ( 6 | "github.com/facebook/ent/dialect/sql" 7 | ) 8 | 9 | // RawEvent is the predicate function for rawevent builders. 10 | type RawEvent func(*sql.Selector) 11 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "github.com/blushft/strana/modules/sink/loader/schema" 7 | "github.com/blushft/strana/modules/sink/loader/store/ent/rawevent" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // The init function reads all schema descriptors with runtime 12 | // code (default values, validators or hooks) and stitches it 13 | // to their package variables. 14 | func init() { 15 | raweventFields := schema.RawEvent{}.Fields() 16 | _ = raweventFields 17 | // raweventDescID is the schema descriptor for id field. 18 | raweventDescID := raweventFields[0].Descriptor() 19 | // rawevent.DefaultID holds the default value on creation for the id field. 20 | rawevent.DefaultID = raweventDescID.Default.(func() uuid.UUID) 21 | } 22 | -------------------------------------------------------------------------------- /modules/sink/loader/store/ent/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package runtime 4 | 5 | // The schema-stitching logic is generated in github.com/blushft/strana/modules/sink/loader/store/ent/runtime.go 6 | 7 | const ( 8 | Version = "v0.4.1" // Version of ent codegen. 9 | Sum = "h1:UXdCoiCbsvnZYnM0j5aF1VBkdwAPCGDAJfC70a8O/tE=" // Sum of ent codegen. 10 | ) 11 | -------------------------------------------------------------------------------- /modules/sink/loader/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/blushft/strana/modules/sink/loader/store/ent" 8 | "github.com/blushft/strana/platform/store" 9 | entsql "github.com/facebook/ent/dialect/sql" 10 | ) 11 | 12 | type Store struct { 13 | client *ent.Client 14 | } 15 | 16 | func New(dbs *store.SQLStore) (*Store, error) { 17 | db := sql.OpenDB(dbs) 18 | drv := entsql.OpenDB(dbs.Dialect(), db) 19 | 20 | s := &Store{ 21 | client: ent.NewClient(ent.Driver(drv)), 22 | } 23 | 24 | if err := s.setup(); err != nil { 25 | return nil, err 26 | } 27 | 28 | return s, nil 29 | } 30 | 31 | func (s *Store) Client() *ent.Client { 32 | return s.client 33 | } 34 | 35 | func (s *Store) Mount(fn func(*Store)) { 36 | fn(s) 37 | } 38 | 39 | func (s *Store) setup() error { 40 | return s.client.Schema.Create(context.TODO()) 41 | } 42 | -------------------------------------------------------------------------------- /modules/sink/reporter/command/event_extract.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | 6 | revent "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/modules/sink/reporter/entity" 8 | "github.com/blushft/strana/modules/sink/reporter/store" 9 | sstore "github.com/blushft/strana/platform/store" 10 | ) 11 | 12 | type EventExtractor struct { 13 | rs *store.Store 14 | 15 | txsvc entity.EventTransaction 16 | actsvc entity.ActionManager 17 | evtsvc entity.EventManager 18 | } 19 | 20 | func NewEventExtractor(s *sstore.SQLStore) (*EventExtractor, error) { 21 | rs, err := store.New(s) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return &EventExtractor{ 27 | txsvc: entity.NewEventTransaction(rs), 28 | actsvc: entity.NewActionService(rs), 29 | evtsvc: entity.NewEventService(rs), 30 | }, nil 31 | } 32 | 33 | func (ee *EventExtractor) Save(evt *revent.Event) error { 34 | if err := ee.txsvc.NewEvent(context.Background(), evt); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /modules/sink/reporter/command/event_report.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/blushft/strana/modules/sink/reporter/entity" 5 | "github.com/blushft/strana/modules/sink/reporter/store" 6 | pstore "github.com/blushft/strana/platform/store" 7 | ) 8 | 9 | type EventReporter struct { 10 | s *store.Store 11 | 12 | evtsvc entity.EventReader 13 | aggsvc entity.EventAggregate 14 | } 15 | 16 | func NewEventReporter(dbs *pstore.SQLStore) (*EventReporter, error) { 17 | rs, err := store.New(dbs) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return &EventReporter{ 23 | s: rs, 24 | evtsvc: entity.NewEventService(rs), 25 | aggsvc: entity.NewEventAggregate(rs), 26 | }, nil 27 | } 28 | 29 | func (rpt *EventReporter) Events(params entity.QueryParams) ([]*entity.Event, error) { 30 | return rpt.evtsvc.List(params) 31 | } 32 | 33 | func (rpt *EventReporter) EventsCount() (int, error) { 34 | return rpt.aggsvc.Count() 35 | } 36 | 37 | func (rpt *EventReporter) EventsWithAction(params entity.QueryParams) ([]*entity.Event, error) { 38 | return rpt.evtsvc.Query(entity.EventHasAction()) 39 | } 40 | 41 | type CategoryCount struct { 42 | Category string `json:"category"` 43 | Count int `json:"count"` 44 | } 45 | 46 | func (rpt *EventReporter) TopActionCatgories(params entity.QueryParams) ([]*CategoryCount, error) { 47 | var cc []*CategoryCount 48 | if err := rpt.aggsvc.GroupBy(entity.TopActionCategories, &cc, params.QueryEvent()...); err != nil { 49 | return nil, err 50 | } 51 | 52 | return cc, nil 53 | } 54 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/aggregate.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/blushft/strana/modules/sink/reporter/store" 7 | "github.com/blushft/strana/modules/sink/reporter/store/ent" 8 | "github.com/blushft/strana/modules/sink/reporter/store/ent/action" 9 | ) 10 | 11 | type EventAggregate interface { 12 | Count(...QueryEvent) (int, error) 13 | GroupBy(ScannableQueryBuild, interface{}, ...QueryEvent) error 14 | } 15 | 16 | type evtAggregate struct { 17 | store *store.Store 18 | } 19 | 20 | func NewEventAggregate(s *store.Store) *evtAggregate { 21 | return &evtAggregate{ 22 | store: s, 23 | } 24 | } 25 | 26 | func (agg *evtAggregate) Count(qs ...QueryEvent) (int, error) { 27 | c := agg.store.Client().Event 28 | 29 | q := c.Query() 30 | 31 | for _, aq := range qs { 32 | aq(q) 33 | } 34 | 35 | return q.Count(context.TODO()) 36 | } 37 | 38 | func (agg *evtAggregate) GroupBy(sq ScannableQueryBuild, v interface{}, qs ...QueryEvent) error { 39 | c := agg.store.Client().Event 40 | 41 | q := c.Query() 42 | 43 | return sq(q, qs...).Scan(context.TODO(), v) 44 | } 45 | 46 | func TopActionCategories(q *ent.EventQuery, qs ...QueryEvent) ScannableQuery { 47 | NewEventQuery(q, qs...) 48 | 49 | return q.QueryAction().GroupBy(action.FieldCategory).Aggregate(ent.Count()) 50 | } 51 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/campaign.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Campaign struct { 4 | ID int 5 | Name string 6 | Source string 7 | Medium string 8 | Term string 9 | Content string 10 | } 11 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/group.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/blushft/strana/modules/sink/reporter/store/ent" 7 | ) 8 | 9 | type Group struct { 10 | ID int `json:"id"` 11 | Name string `json:"name"` 12 | Meta map[string]string `json:"meta"` 13 | Users []*User `json:"users"` 14 | Groups []*Group `json:"groups"` 15 | } 16 | 17 | type GroupReader interface { 18 | List(QueryParams) ([]*Group, error) 19 | Get(id int) (*Group, error) 20 | } 21 | 22 | type GroupWriter interface { 23 | Create(*Group) error 24 | Update(*Group) error 25 | Delete(*Group) error 26 | } 27 | 28 | type groupRepo interface { 29 | GroupReader 30 | GroupWriter 31 | } 32 | 33 | type GroupManager interface { 34 | groupRepo 35 | } 36 | 37 | type groupManager struct { 38 | c *ent.GroupClient 39 | } 40 | 41 | func NewGroupService(c *ent.GroupClient) *groupManager { 42 | return &groupManager{ 43 | c: c, 44 | } 45 | } 46 | 47 | func (mgr *groupManager) List(qp QueryParams) ([]*Group, error) { 48 | var res []*Group 49 | 50 | q := mgr.c.Query() 51 | 52 | recs, err := q.All(context.TODO()) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | for _, rec := range recs { 58 | res = append(res, groupSchemaToEntity(rec)) 59 | } 60 | 61 | return res, nil 62 | } 63 | 64 | func groupEntityCreate(c *ent.GroupClient, e *Group) *ent.GroupCreate { 65 | return c.Create(). 66 | SetName(e.Name) 67 | } 68 | 69 | func groupEntityUpdate(c *ent.GroupClient, e *Group) *ent.GroupUpdate { 70 | return c.Update(). 71 | SetName(e.Name) 72 | } 73 | 74 | func groupSchemaToEntity(sch *ent.Group) *Group { 75 | return &Group{ 76 | ID: sch.ID, 77 | Name: sch.Name, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/pagestats.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type PageStats struct { 8 | New bool `db:"-" json:"-"` 9 | SiteID int64 `db:"site_id" json:"-"` 10 | HostnameID int64 `db:"hostname_id" json:"-"` 11 | PathnameID int64 `db:"pathname_id" json:"-"` 12 | Hostname string `db:"hostname"` 13 | Pathname string `db:"pathname"` 14 | Pageviews int64 `db:"pageviews"` 15 | Visitors int64 `db:"visitors"` 16 | Entries int64 `db:"entries"` 17 | BounceRate float64 `db:"bounce_rate"` 18 | AvgDuration float64 `db:"avg_duration"` 19 | KnownDurations int64 `db:"known_durations"` 20 | Date time.Time `db:"ts" json:",omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/query.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/blushft/strana/modules/sink/reporter/store/ent" 8 | "github.com/blushft/strana/modules/sink/reporter/store/ent/event" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | const ( 13 | DefaultLimit = 10 14 | DefaultOffset = 0 15 | ) 16 | 17 | type QueryParams struct { 18 | DeviceIDs []string `json:"cid" query:"cid"` 19 | SessionIDs []uuid.UUID `json:"sid" query:"sid"` 20 | UserIDs []uuid.UUID `json:"uid" query:"uid"` 21 | GroupIDs []int `json:"gid" query:"gid"` 22 | Offset int `json:"offset" query:"offset"` 23 | Limit int `json:"limit" query:"limit"` 24 | Start int `json:"start" query:"start"` 25 | End int `json:"end" query:"end"` 26 | Params map[string]string `json:"params" query:"params"` 27 | } 28 | 29 | func (qp QueryParams) QueryEvent() []QueryEvent { 30 | var qs []QueryEvent 31 | 32 | return qs 33 | } 34 | 35 | type QueryEvent func(q *ent.EventQuery) 36 | 37 | type EventQuery interface { 38 | All(context.Context) ([]*ent.Event, error) 39 | } 40 | type ScannableQuery interface { 41 | Scan(context.Context, interface{}) error 42 | } 43 | 44 | type EventQueryBuild func(q *ent.EventQuery, qs ...QueryEvent) 45 | type ScannableQueryBuild func(q *ent.EventQuery, qs ...QueryEvent) ScannableQuery 46 | 47 | func NewEventQuery(q *ent.EventQuery, qs ...QueryEvent) { 48 | for _, eq := range qs { 49 | eq(q) 50 | } 51 | } 52 | 53 | func EventLimit(l int) QueryEvent { 54 | return func(eq *ent.EventQuery) { 55 | eq.Limit(l) 56 | } 57 | } 58 | 59 | func EventOffset(o int) QueryEvent { 60 | return func(eq *ent.EventQuery) { 61 | eq.Offset(o) 62 | } 63 | } 64 | 65 | func EventHasAction() QueryEvent { 66 | return func(eq *ent.EventQuery) { 67 | eq.Where(event.HasAction()) 68 | } 69 | } 70 | 71 | func EventTimeBefore(t time.Time) QueryEvent { 72 | return func(eq *ent.EventQuery) { 73 | eq.Where(event.TimestampLT(t)) 74 | } 75 | } 76 | 77 | func EventTimeAfter(t time.Time) QueryEvent { 78 | return func(eq *ent.EventQuery) { 79 | eq.Where(event.TimestampGT(t)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/referrerstats.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ReferrerStats struct { 8 | New bool `db:"-" json:"-"` 9 | SiteID int64 `db:"site_id" json:"-"` 10 | HostnameID int64 `db:"hostname_id" json:"-"` 11 | PathnameID int64 `db:"pathname_id" json:"-"` 12 | Hostname string `db:"hostname"` 13 | Pathname string `db:"pathname"` 14 | Group string `db:"groupname"` 15 | Visitors int64 `db:"visitors"` 16 | Pageviews int64 `db:"pageviews"` 17 | BounceRate float64 `db:"bounce_rate"` 18 | AvgDuration float64 `db:"avg_duration"` 19 | KnownDurations int64 `db:"known_durations"` 20 | Date time.Time `db:"ts" json:",omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /modules/sink/reporter/entity/sitestats.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type SiteStats struct { 9 | New bool `db:"-" json:"-" ` 10 | SiteID int64 `db:"site_id" json:"-"` 11 | Visitors int64 `db:"visitors"` 12 | Pageviews int64 `db:"pageviews"` 13 | Sessions int64 `db:"sessions"` 14 | BounceRate float64 `db:"bounce_rate"` 15 | AvgDuration float64 `db:"avg_duration"` 16 | KnownDurations int64 `db:"known_durations" json:",omitempty"` 17 | Date time.Time `db:"ts" json:",omitempty"` 18 | } 19 | 20 | func (s *SiteStats) FormattedDuration() string { 21 | return fmt.Sprintf("%d:%d", int(s.AvgDuration/60.00), (int(s.AvgDuration) % 60)) 22 | } 23 | -------------------------------------------------------------------------------- /modules/sink/reporter/gen.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | //go:generate go run github.com/facebook/ent/cmd/entc generate ./schema --target ./store/ent 4 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/action.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Action holds the schema definition for the Action entity. 10 | type Action struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Action. 15 | func (Action) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("action"), 18 | field.String("category"), 19 | field.String("action_label").StorageKey("label").Optional(), 20 | field.String("property").Optional(), 21 | field.Bytes("value").Optional(), 22 | } 23 | } 24 | 25 | // Edges of the Action. 26 | func (Action) Edges() []ent.Edge { 27 | return []ent.Edge{ 28 | edge.From("event", Event.Type).Ref("action").Unique().Required(), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/alias.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Alias holds the schema definition for the Alias entity. 10 | type Alias struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Alias. 15 | func (Alias) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("from"), 18 | field.String("to"), 19 | } 20 | } 21 | 22 | // Edges of the Alias. 23 | func (Alias) Edges() []ent.Edge { 24 | return []ent.Edge{ 25 | edge.From("event", Event.Type).Ref("alias").Unique(), 26 | edge.From("user", User.Type).Ref("aliases").Unique(), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/app.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | "github.com/facebook/ent/schema/index" 8 | ) 9 | 10 | // App holds the schema definition for the App entity. 11 | type App struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the App. 16 | func (App) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("name"), 19 | field.String("version").Optional(), 20 | field.String("build").Optional(), 21 | field.String("namespace").Optional(), 22 | field.JSON("properties", map[string]interface{}{}).Optional(), 23 | } 24 | } 25 | 26 | // Edges of the App. 27 | func (App) Edges() []ent.Edge { 28 | return []ent.Edge{ 29 | edge.From("events", Event.Type).Ref("app"), 30 | } 31 | } 32 | 33 | func (App) Indexes() []ent.Index { 34 | return []ent.Index{ 35 | index.Fields("name", "version", "build").Unique(), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/browser.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Browser holds the schema definition for the Browser entity. 10 | type Browser struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Browser. 15 | func (Browser) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("version"), 19 | field.String("useragent").Optional(), 20 | } 21 | } 22 | 23 | // Edges of the Browser. 24 | func (Browser) Edges() []ent.Edge { 25 | return []ent.Edge{ 26 | edge.From("event", Event.Type).Ref("browser").Unique(), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/campaign.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Campaign holds the schema definition for the Campaign entity. 10 | type Campaign struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Campaign. 15 | func (Campaign) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("source").Optional(), 19 | field.String("medium").Optional(), 20 | field.String("term").Optional(), 21 | field.String("content").Optional(), 22 | } 23 | } 24 | 25 | // Edges of the Campaign. 26 | func (Campaign) Edges() []ent.Edge { 27 | return []ent.Edge{ 28 | edge.From("event", Event.Type).Ref("campaign"), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/connectivity.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Connectivity holds the schema definition for the Connectivity entity. 10 | type Connectivity struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Connectivity. 15 | func (Connectivity) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.Bool("bluetooth"), 18 | field.Bool("cellular"), 19 | field.Bool("wifi"), 20 | field.Bool("ethernet"), 21 | field.Bool("carrier"), 22 | field.Bool("isp"), 23 | } 24 | } 25 | 26 | // Edges of the Connectivity. 27 | func (Connectivity) Edges() []ent.Edge { 28 | return []ent.Edge{ 29 | edge.From("event", Event.Type).Ref("connectivity").Unique(), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/device.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Device holds the schema definition for the Device entity. 10 | type Device struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Device. 15 | func (Device) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("id"), 18 | field.String("manufacturer").Optional(), 19 | field.String("model").Optional(), 20 | field.String("name").Optional(), 21 | field.String("type").Optional(), 22 | field.String("version").Optional(), 23 | field.Bool("mobile").Optional(), 24 | field.Bool("tablet").Optional(), 25 | field.Bool("desktop").Optional(), 26 | field.JSON("properties", map[string]interface{}{}).Optional(), 27 | } 28 | } 29 | 30 | // Edges of the Device. 31 | func (Device) Edges() []ent.Edge { 32 | return []ent.Edge{ 33 | edge.From("events", Event.Type).Ref("device"), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/event.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Event holds the schema definition for the Event entity. 11 | type Event struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Event. 16 | func (Event) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.UUID("id", uuid.UUID{}), 19 | field.String("tracking_id"), 20 | field.Enum("event"). 21 | Values( 22 | "action", 23 | "alias", 24 | "group", 25 | "identify", 26 | "pageview", 27 | "screenview", 28 | "session", 29 | "timing", 30 | "transaction", 31 | ), 32 | field.Bool("non_interactive"), 33 | field.String("channel").Optional(), 34 | field.String("platform").Optional(), 35 | field.JSON("properties", map[string]interface{}{}).Optional(), 36 | field.Time("timestamp"), 37 | } 38 | } 39 | 40 | // Edges of the Event. 41 | func (Event) Edges() []ent.Edge { 42 | return []ent.Edge{ 43 | edge.To("action", Action.Type).Unique(), 44 | edge.To("alias", Alias.Type).Unique(), 45 | edge.To("app", App.Type).Unique(), 46 | edge.To("browser", Browser.Type).Unique(), 47 | edge.To("campaign", Campaign.Type).Unique(), 48 | edge.To("connectivity", Connectivity.Type).Unique(), 49 | edge.To("device", Device.Type).Unique(), 50 | edge.To("extra", Extra.Type).Unique(), 51 | edge.To("group", Group.Type).Unique(), 52 | edge.To("library", Library.Type).Unique(), 53 | edge.To("location", Location.Type).Unique(), 54 | edge.To("network", Network.Type).Unique(), 55 | edge.To("os", OSContext.Type).Unique(), 56 | edge.To("page", Page.Type).Unique(), 57 | edge.To("referrer", Referrer.Type).Unique(), 58 | edge.To("screen", Screen.Type).Unique(), 59 | edge.To("session", Session.Type).Unique(), 60 | edge.To("timing", Timing.Type).Unique(), 61 | edge.To("viewport", Viewport.Type).Unique(), 62 | edge.To("user", User.Type).Unique(), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/extra.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Extra holds the schema definition for the Extra entity. 10 | type Extra struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Extra. 15 | func (Extra) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.JSON("values", map[string]interface{}{}), 18 | } 19 | } 20 | 21 | // Edges of the Extra. 22 | func (Extra) Edges() []ent.Edge { 23 | return []ent.Edge{ 24 | edge.From("event", Event.Type).Ref("extra"), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/group.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Group holds the schema definition for the Group entity. 10 | type Group struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Group. 15 | func (Group) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | } 19 | } 20 | 21 | // Edges of the Group. 22 | func (Group) Edges() []ent.Edge { 23 | return []ent.Edge{ 24 | edge.From("events", Event.Type).Ref("group"), 25 | edge.To("users", User.Type), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/library.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Library holds the schema definition for the Library entity. 10 | type Library struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Library. 15 | func (Library) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("version").Optional(), 19 | } 20 | } 21 | 22 | // Edges of the Library. 23 | func (Library) Edges() []ent.Edge { 24 | return []ent.Edge{ 25 | edge.From("events", Event.Type).Ref("library"), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/location.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Location holds the schema definition for the Location entity. 10 | type Location struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Location. 15 | func (Location) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("street").Optional(), 18 | field.String("city").Optional(), 19 | field.String("state").Optional(), 20 | field.String("postalcode").Optional(), 21 | field.String("region").Optional(), 22 | field.String("locale").Optional(), 23 | field.String("country").Optional(), 24 | field.Float("longitude").Optional(), 25 | field.Float("latitude").Optional(), 26 | field.String("timezone").Optional(), 27 | } 28 | } 29 | 30 | // Edges of the Location. 31 | func (Location) Edges() []ent.Edge { 32 | return []ent.Edge{ 33 | edge.From("events", Event.Type).Ref("location"), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/network.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Network holds the schema definition for the Network entity. 10 | type Network struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Network. 15 | func (Network) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("ip"), 18 | field.String("useragent").Optional(), 19 | } 20 | } 21 | 22 | // Edges of the Network. 23 | func (Network) Edges() []ent.Edge { 24 | return []ent.Edge{ 25 | edge.From("events", Event.Type).Ref("network"), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/os.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // OSContext holds the schema definition for the OSContext entity. 10 | type OSContext struct { 11 | ent.Schema 12 | } 13 | 14 | func (OSContext) Config() ent.Config { 15 | return ent.Config{ 16 | Table: "os", 17 | } 18 | } 19 | 20 | // Fields of the OSContext. 21 | func (OSContext) Fields() []ent.Field { 22 | return []ent.Field{ 23 | field.String("name"), 24 | field.String("family"), 25 | field.String("platform").Optional(), 26 | field.String("version"), 27 | } 28 | } 29 | 30 | // Edges of the OSContext. 31 | func (OSContext) Edges() []ent.Edge { 32 | return []ent.Edge{ 33 | edge.From("events", Event.Type).Ref("os"), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/page.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Page holds the schema definition for the Page entity. 10 | type Page struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Page. 15 | func (Page) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("hostname"), 18 | field.String("path"), 19 | field.String("referrer").Optional(), 20 | field.String("search").Optional(), 21 | field.String("title").Optional(), 22 | field.String("hash").Optional(), 23 | } 24 | } 25 | 26 | // Edges of the Page. 27 | func (Page) Edges() []ent.Edge { 28 | return []ent.Edge{ 29 | edge.From("events", Event.Type).Ref("page"), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/referrer.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Referrer holds the schema definition for the Referrer entity. 10 | type Referrer struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Referrer. 15 | func (Referrer) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("type").Optional(), 19 | field.String("hostname").Optional(), 20 | field.String("link").Optional(), 21 | } 22 | } 23 | 24 | // Edges of the Referrer. 25 | func (Referrer) Edges() []ent.Edge { 26 | return []ent.Edge{ 27 | edge.From("events", Event.Type).Ref("referrer"), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/screen.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Screen holds the schema definition for the Screen entity. 10 | type Screen struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Screen. 15 | func (Screen) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("category").Optional(), 19 | } 20 | } 21 | 22 | // Edges of the Screen. 23 | func (Screen) Edges() []ent.Edge { 24 | return []ent.Edge{ 25 | edge.From("events", Event.Type).Ref("screen"), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/session.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Session holds the schema definition for the Session entity. 11 | type Session struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Session. 16 | func (Session) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.UUID("id", uuid.UUID{}), 19 | field.Bool("new_user"), 20 | field.Bool("is_unique"), 21 | field.Bool("is_bounce"), 22 | field.Bool("is_finished"), 23 | field.Int("duration").Optional(), 24 | field.Time("started_at"), 25 | field.Time("finished_at").Nillable().Optional(), 26 | } 27 | } 28 | 29 | // Edges of the Session. 30 | func (Session) Edges() []ent.Edge { 31 | return []ent.Edge{ 32 | edge.From("events", Event.Type).Ref("session"), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/timing.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Timing holds the schema definition for the Timing entity. 10 | type Timing struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Timing. 15 | func (Timing) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("category"), 18 | field.String("timing_label").StorageKey("label"), 19 | field.String("unit"), 20 | field.String("variable"), 21 | field.Float("value"), 22 | } 23 | } 24 | 25 | // Edges of the Timing. 26 | func (Timing) Edges() []ent.Edge { 27 | return []ent.Edge{ 28 | edge.From("events", Event.Type).Ref("timing"), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // User holds the schema definition for the User entity. 10 | type User struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the User. 15 | func (User) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("id"), 18 | field.Bool("is_anonymous"), 19 | field.String("name").Optional(), 20 | field.String("title").Optional(), 21 | field.String("first_name").Optional(), 22 | field.String("last_name").Optional(), 23 | field.String("email").Optional(), 24 | field.String("username").Optional(), 25 | field.Int("age").Optional(), 26 | field.Time("birthday").Optional().Nillable(), 27 | field.Enum("gender").NamedValues("Male", "M", "Female", "F", "Other", "O").Optional(), 28 | field.String("phone").Optional(), 29 | field.String("website").Optional(), 30 | field.JSON("extra", map[string]interface{}{}).Optional(), 31 | } 32 | } 33 | 34 | // Edges of the User. 35 | func (User) Edges() []ent.Edge { 36 | return []ent.Edge{ 37 | edge.To("aliases", Alias.Type), 38 | edge.From("events", Event.Type).Ref("user"), 39 | edge.From("groups", Group.Type).Ref("users"), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/sink/reporter/schema/viewport.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/facebook/ent" 5 | "github.com/facebook/ent/schema/edge" 6 | "github.com/facebook/ent/schema/field" 7 | ) 8 | 9 | // Viewport holds the schema definition for the Viewport entity. 10 | type Viewport struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the Viewport. 15 | func (Viewport) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.Int("density"), 18 | field.Int("width"), 19 | field.Int("height"), 20 | } 21 | } 22 | 23 | // Edges of the Viewport. 24 | func (Viewport) Edges() []ent.Edge { 25 | return []ent.Edge{ 26 | edge.From("events", Event.Type).Ref("viewport"), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/action/action.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package action 4 | 5 | const ( 6 | // Label holds the string label denoting the action type in the database. 7 | Label = "action" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldAction holds the string denoting the action field in the database. 11 | FieldAction = "action" 12 | // FieldCategory holds the string denoting the category field in the database. 13 | FieldCategory = "category" 14 | // FieldActionLabel holds the string denoting the action_label field in the database. 15 | FieldActionLabel = "label" 16 | // FieldProperty holds the string denoting the property field in the database. 17 | FieldProperty = "property" 18 | // FieldValue holds the string denoting the value field in the database. 19 | FieldValue = "value" 20 | 21 | // EdgeEvent holds the string denoting the event edge name in mutations. 22 | EdgeEvent = "event" 23 | 24 | // Table holds the table name of the action in the database. 25 | Table = "actions" 26 | // EventTable is the table the holds the event relation/edge. 27 | EventTable = "actions" 28 | // EventInverseTable is the table name for the Event entity. 29 | // It exists in this package in order to avoid circular dependency with the "event" package. 30 | EventInverseTable = "events" 31 | // EventColumn is the table column denoting the event relation/edge. 32 | EventColumn = "event_action" 33 | ) 34 | 35 | // Columns holds all SQL columns for action fields. 36 | var Columns = []string{ 37 | FieldID, 38 | FieldAction, 39 | FieldCategory, 40 | FieldActionLabel, 41 | FieldProperty, 42 | FieldValue, 43 | } 44 | 45 | // ForeignKeys holds the SQL foreign-keys that are owned by the Action type. 46 | var ForeignKeys = []string{ 47 | "event_action", 48 | } 49 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/alias/alias.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package alias 4 | 5 | const ( 6 | // Label holds the string label denoting the alias type in the database. 7 | Label = "alias" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldFrom holds the string denoting the from field in the database. 11 | FieldFrom = "from" 12 | // FieldTo holds the string denoting the to field in the database. 13 | FieldTo = "to" 14 | 15 | // EdgeEvent holds the string denoting the event edge name in mutations. 16 | EdgeEvent = "event" 17 | // EdgeUser holds the string denoting the user edge name in mutations. 18 | EdgeUser = "user" 19 | 20 | // Table holds the table name of the alias in the database. 21 | Table = "alias" 22 | // EventTable is the table the holds the event relation/edge. 23 | EventTable = "alias" 24 | // EventInverseTable is the table name for the Event entity. 25 | // It exists in this package in order to avoid circular dependency with the "event" package. 26 | EventInverseTable = "events" 27 | // EventColumn is the table column denoting the event relation/edge. 28 | EventColumn = "event_alias" 29 | // UserTable is the table the holds the user relation/edge. 30 | UserTable = "alias" 31 | // UserInverseTable is the table name for the User entity. 32 | // It exists in this package in order to avoid circular dependency with the "user" package. 33 | UserInverseTable = "users" 34 | // UserColumn is the table column denoting the user relation/edge. 35 | UserColumn = "user_aliases" 36 | ) 37 | 38 | // Columns holds all SQL columns for alias fields. 39 | var Columns = []string{ 40 | FieldID, 41 | FieldFrom, 42 | FieldTo, 43 | } 44 | 45 | // ForeignKeys holds the SQL foreign-keys that are owned by the Alias type. 46 | var ForeignKeys = []string{ 47 | "event_alias", 48 | "user_aliases", 49 | } 50 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/app/app.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package app 4 | 5 | const ( 6 | // Label holds the string label denoting the app type in the database. 7 | Label = "app" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldVersion holds the string denoting the version field in the database. 13 | FieldVersion = "version" 14 | // FieldBuild holds the string denoting the build field in the database. 15 | FieldBuild = "build" 16 | // FieldNamespace holds the string denoting the namespace field in the database. 17 | FieldNamespace = "namespace" 18 | // FieldProperties holds the string denoting the properties field in the database. 19 | FieldProperties = "properties" 20 | 21 | // EdgeEvents holds the string denoting the events edge name in mutations. 22 | EdgeEvents = "events" 23 | 24 | // Table holds the table name of the app in the database. 25 | Table = "apps" 26 | // EventsTable is the table the holds the events relation/edge. 27 | EventsTable = "events" 28 | // EventsInverseTable is the table name for the Event entity. 29 | // It exists in this package in order to avoid circular dependency with the "event" package. 30 | EventsInverseTable = "events" 31 | // EventsColumn is the table column denoting the events relation/edge. 32 | EventsColumn = "event_app" 33 | ) 34 | 35 | // Columns holds all SQL columns for app fields. 36 | var Columns = []string{ 37 | FieldID, 38 | FieldName, 39 | FieldVersion, 40 | FieldBuild, 41 | FieldNamespace, 42 | FieldProperties, 43 | } 44 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/browser/browser.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package browser 4 | 5 | const ( 6 | // Label holds the string label denoting the browser type in the database. 7 | Label = "browser" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldVersion holds the string denoting the version field in the database. 13 | FieldVersion = "version" 14 | // FieldUseragent holds the string denoting the useragent field in the database. 15 | FieldUseragent = "useragent" 16 | 17 | // EdgeEvent holds the string denoting the event edge name in mutations. 18 | EdgeEvent = "event" 19 | 20 | // Table holds the table name of the browser in the database. 21 | Table = "browsers" 22 | // EventTable is the table the holds the event relation/edge. 23 | EventTable = "browsers" 24 | // EventInverseTable is the table name for the Event entity. 25 | // It exists in this package in order to avoid circular dependency with the "event" package. 26 | EventInverseTable = "events" 27 | // EventColumn is the table column denoting the event relation/edge. 28 | EventColumn = "event_browser" 29 | ) 30 | 31 | // Columns holds all SQL columns for browser fields. 32 | var Columns = []string{ 33 | FieldID, 34 | FieldName, 35 | FieldVersion, 36 | FieldUseragent, 37 | } 38 | 39 | // ForeignKeys holds the SQL foreign-keys that are owned by the Browser type. 40 | var ForeignKeys = []string{ 41 | "event_browser", 42 | } 43 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/campaign/campaign.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package campaign 4 | 5 | const ( 6 | // Label holds the string label denoting the campaign type in the database. 7 | Label = "campaign" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldSource holds the string denoting the source field in the database. 13 | FieldSource = "source" 14 | // FieldMedium holds the string denoting the medium field in the database. 15 | FieldMedium = "medium" 16 | // FieldTerm holds the string denoting the term field in the database. 17 | FieldTerm = "term" 18 | // FieldContent holds the string denoting the content field in the database. 19 | FieldContent = "content" 20 | 21 | // EdgeEvent holds the string denoting the event edge name in mutations. 22 | EdgeEvent = "event" 23 | 24 | // Table holds the table name of the campaign in the database. 25 | Table = "campaigns" 26 | // EventTable is the table the holds the event relation/edge. 27 | EventTable = "events" 28 | // EventInverseTable is the table name for the Event entity. 29 | // It exists in this package in order to avoid circular dependency with the "event" package. 30 | EventInverseTable = "events" 31 | // EventColumn is the table column denoting the event relation/edge. 32 | EventColumn = "event_campaign" 33 | ) 34 | 35 | // Columns holds all SQL columns for campaign fields. 36 | var Columns = []string{ 37 | FieldID, 38 | FieldName, 39 | FieldSource, 40 | FieldMedium, 41 | FieldTerm, 42 | FieldContent, 43 | } 44 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/config.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "github.com/facebook/ent" 7 | "github.com/facebook/ent/dialect" 8 | ) 9 | 10 | // Option function to configure the client. 11 | type Option func(*config) 12 | 13 | // Config is the configuration for the client and its builder. 14 | type config struct { 15 | // driver used for executing database requests. 16 | driver dialect.Driver 17 | // debug enable a debug logging. 18 | debug bool 19 | // log used for logging on debug mode. 20 | log func(...interface{}) 21 | // hooks to execute on mutations. 22 | hooks *hooks 23 | } 24 | 25 | // hooks per client, for fast access. 26 | type hooks struct { 27 | Action []ent.Hook 28 | Alias []ent.Hook 29 | App []ent.Hook 30 | Browser []ent.Hook 31 | Campaign []ent.Hook 32 | Connectivity []ent.Hook 33 | Device []ent.Hook 34 | Event []ent.Hook 35 | Extra []ent.Hook 36 | Group []ent.Hook 37 | Library []ent.Hook 38 | Location []ent.Hook 39 | Network []ent.Hook 40 | OSContext []ent.Hook 41 | Page []ent.Hook 42 | Referrer []ent.Hook 43 | Screen []ent.Hook 44 | Session []ent.Hook 45 | Timing []ent.Hook 46 | User []ent.Hook 47 | Viewport []ent.Hook 48 | } 49 | 50 | // Options applies the options on the config object. 51 | func (c *config) options(opts ...Option) { 52 | for _, opt := range opts { 53 | opt(c) 54 | } 55 | if c.debug { 56 | c.driver = dialect.Debug(c.driver, c.log) 57 | } 58 | } 59 | 60 | // Debug enables debug logging on the ent.Driver. 61 | func Debug() Option { 62 | return func(c *config) { 63 | c.debug = true 64 | } 65 | } 66 | 67 | // Log sets the logging function for debug mode. 68 | func Log(fn func(...interface{})) Option { 69 | return func(c *config) { 70 | c.log = fn 71 | } 72 | } 73 | 74 | // Driver configures the client driver. 75 | func Driver(driver dialect.Driver) Option { 76 | return func(c *config) { 77 | c.driver = driver 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/connectivity/connectivity.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package connectivity 4 | 5 | const ( 6 | // Label holds the string label denoting the connectivity type in the database. 7 | Label = "connectivity" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldBluetooth holds the string denoting the bluetooth field in the database. 11 | FieldBluetooth = "bluetooth" 12 | // FieldCellular holds the string denoting the cellular field in the database. 13 | FieldCellular = "cellular" 14 | // FieldWifi holds the string denoting the wifi field in the database. 15 | FieldWifi = "wifi" 16 | // FieldEthernet holds the string denoting the ethernet field in the database. 17 | FieldEthernet = "ethernet" 18 | // FieldCarrier holds the string denoting the carrier field in the database. 19 | FieldCarrier = "carrier" 20 | // FieldIsp holds the string denoting the isp field in the database. 21 | FieldIsp = "isp" 22 | 23 | // EdgeEvent holds the string denoting the event edge name in mutations. 24 | EdgeEvent = "event" 25 | 26 | // Table holds the table name of the connectivity in the database. 27 | Table = "connectivities" 28 | // EventTable is the table the holds the event relation/edge. 29 | EventTable = "connectivities" 30 | // EventInverseTable is the table name for the Event entity. 31 | // It exists in this package in order to avoid circular dependency with the "event" package. 32 | EventInverseTable = "events" 33 | // EventColumn is the table column denoting the event relation/edge. 34 | EventColumn = "event_connectivity" 35 | ) 36 | 37 | // Columns holds all SQL columns for connectivity fields. 38 | var Columns = []string{ 39 | FieldID, 40 | FieldBluetooth, 41 | FieldCellular, 42 | FieldWifi, 43 | FieldEthernet, 44 | FieldCarrier, 45 | FieldIsp, 46 | } 47 | 48 | // ForeignKeys holds the SQL foreign-keys that are owned by the Connectivity type. 49 | var ForeignKeys = []string{ 50 | "event_connectivity", 51 | } 52 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type clientCtxKey struct{} 10 | 11 | // FromContext returns the Client stored in a context, or nil if there isn't one. 12 | func FromContext(ctx context.Context) *Client { 13 | c, _ := ctx.Value(clientCtxKey{}).(*Client) 14 | return c 15 | } 16 | 17 | // NewContext returns a new context with the given Client attached. 18 | func NewContext(parent context.Context, c *Client) context.Context { 19 | return context.WithValue(parent, clientCtxKey{}, c) 20 | } 21 | 22 | type txCtxKey struct{} 23 | 24 | // TxFromContext returns the Tx stored in a context, or nil if there isn't one. 25 | func TxFromContext(ctx context.Context) *Tx { 26 | tx, _ := ctx.Value(txCtxKey{}).(*Tx) 27 | return tx 28 | } 29 | 30 | // NewTxContext returns a new context with the given Client attached. 31 | func NewTxContext(parent context.Context, tx *Tx) context.Context { 32 | return context.WithValue(parent, txCtxKey{}, tx) 33 | } 34 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/device/device.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package device 4 | 5 | const ( 6 | // Label holds the string label denoting the device type in the database. 7 | Label = "device" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldManufacturer holds the string denoting the manufacturer field in the database. 11 | FieldManufacturer = "manufacturer" 12 | // FieldModel holds the string denoting the model field in the database. 13 | FieldModel = "model" 14 | // FieldName holds the string denoting the name field in the database. 15 | FieldName = "name" 16 | // FieldType holds the string denoting the type field in the database. 17 | FieldType = "type" 18 | // FieldVersion holds the string denoting the version field in the database. 19 | FieldVersion = "version" 20 | // FieldMobile holds the string denoting the mobile field in the database. 21 | FieldMobile = "mobile" 22 | // FieldTablet holds the string denoting the tablet field in the database. 23 | FieldTablet = "tablet" 24 | // FieldDesktop holds the string denoting the desktop field in the database. 25 | FieldDesktop = "desktop" 26 | // FieldProperties holds the string denoting the properties field in the database. 27 | FieldProperties = "properties" 28 | 29 | // EdgeEvents holds the string denoting the events edge name in mutations. 30 | EdgeEvents = "events" 31 | 32 | // Table holds the table name of the device in the database. 33 | Table = "devices" 34 | // EventsTable is the table the holds the events relation/edge. 35 | EventsTable = "events" 36 | // EventsInverseTable is the table name for the Event entity. 37 | // It exists in this package in order to avoid circular dependency with the "event" package. 38 | EventsInverseTable = "events" 39 | // EventsColumn is the table column denoting the events relation/edge. 40 | EventsColumn = "event_device" 41 | ) 42 | 43 | // Columns holds all SQL columns for device fields. 44 | var Columns = []string{ 45 | FieldID, 46 | FieldManufacturer, 47 | FieldModel, 48 | FieldName, 49 | FieldType, 50 | FieldVersion, 51 | FieldMobile, 52 | FieldTablet, 53 | FieldDesktop, 54 | FieldProperties, 55 | } 56 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/enttest/enttest.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package enttest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/blushft/strana/modules/sink/reporter/store/ent" 9 | // required by schema hooks. 10 | _ "github.com/blushft/strana/modules/sink/reporter/store/ent/runtime" 11 | 12 | "github.com/facebook/ent/dialect/sql/schema" 13 | ) 14 | 15 | type ( 16 | // TestingT is the interface that is shared between 17 | // testing.T and testing.B and used by enttest. 18 | TestingT interface { 19 | FailNow() 20 | Error(...interface{}) 21 | } 22 | 23 | // Option configures client creation. 24 | Option func(*options) 25 | 26 | options struct { 27 | opts []ent.Option 28 | migrateOpts []schema.MigrateOption 29 | } 30 | ) 31 | 32 | // WithOptions forwards options to client creation. 33 | func WithOptions(opts ...ent.Option) Option { 34 | return func(o *options) { 35 | o.opts = append(o.opts, opts...) 36 | } 37 | } 38 | 39 | // WithMigrateOptions forwards options to auto migration. 40 | func WithMigrateOptions(opts ...schema.MigrateOption) Option { 41 | return func(o *options) { 42 | o.migrateOpts = append(o.migrateOpts, opts...) 43 | } 44 | } 45 | 46 | func newOptions(opts []Option) *options { 47 | o := &options{} 48 | for _, opt := range opts { 49 | opt(o) 50 | } 51 | return o 52 | } 53 | 54 | // Open calls ent.Open and auto-run migration. 55 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { 56 | o := newOptions(opts) 57 | c, err := ent.Open(driverName, dataSourceName, o.opts...) 58 | if err != nil { 59 | t.Error(err) 60 | t.FailNow() 61 | } 62 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 63 | t.Error(err) 64 | t.FailNow() 65 | } 66 | return c 67 | } 68 | 69 | // NewClient calls ent.NewClient and auto-run migration. 70 | func NewClient(t TestingT, opts ...Option) *ent.Client { 71 | o := newOptions(opts) 72 | c := ent.NewClient(o.opts...) 73 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil { 74 | t.Error(err) 75 | t.FailNow() 76 | } 77 | return c 78 | } 79 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/extra/extra.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package extra 4 | 5 | const ( 6 | // Label holds the string label denoting the extra type in the database. 7 | Label = "extra" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldValues holds the string denoting the values field in the database. 11 | FieldValues = "values" 12 | 13 | // EdgeEvent holds the string denoting the event edge name in mutations. 14 | EdgeEvent = "event" 15 | 16 | // Table holds the table name of the extra in the database. 17 | Table = "extras" 18 | // EventTable is the table the holds the event relation/edge. 19 | EventTable = "events" 20 | // EventInverseTable is the table name for the Event entity. 21 | // It exists in this package in order to avoid circular dependency with the "event" package. 22 | EventInverseTable = "events" 23 | // EventColumn is the table column denoting the event relation/edge. 24 | EventColumn = "event_extra" 25 | ) 26 | 27 | // Columns holds all SQL columns for extra fields. 28 | var Columns = []string{ 29 | FieldID, 30 | FieldValues, 31 | } 32 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/group/group.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package group 4 | 5 | const ( 6 | // Label holds the string label denoting the group type in the database. 7 | Label = "group" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | 13 | // EdgeEvents holds the string denoting the events edge name in mutations. 14 | EdgeEvents = "events" 15 | // EdgeUsers holds the string denoting the users edge name in mutations. 16 | EdgeUsers = "users" 17 | 18 | // Table holds the table name of the group in the database. 19 | Table = "groups" 20 | // EventsTable is the table the holds the events relation/edge. 21 | EventsTable = "events" 22 | // EventsInverseTable is the table name for the Event entity. 23 | // It exists in this package in order to avoid circular dependency with the "event" package. 24 | EventsInverseTable = "events" 25 | // EventsColumn is the table column denoting the events relation/edge. 26 | EventsColumn = "event_group" 27 | // UsersTable is the table the holds the users relation/edge. The primary key declared below. 28 | UsersTable = "group_users" 29 | // UsersInverseTable is the table name for the User entity. 30 | // It exists in this package in order to avoid circular dependency with the "user" package. 31 | UsersInverseTable = "users" 32 | ) 33 | 34 | // Columns holds all SQL columns for group fields. 35 | var Columns = []string{ 36 | FieldID, 37 | FieldName, 38 | } 39 | 40 | var ( 41 | // UsersPrimaryKey and UsersColumn2 are the table columns denoting the 42 | // primary key for the users relation (M2M). 43 | UsersPrimaryKey = []string{"group_id", "user_id"} 44 | ) 45 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/library/library.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package library 4 | 5 | const ( 6 | // Label holds the string label denoting the library type in the database. 7 | Label = "library" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldVersion holds the string denoting the version field in the database. 13 | FieldVersion = "version" 14 | 15 | // EdgeEvents holds the string denoting the events edge name in mutations. 16 | EdgeEvents = "events" 17 | 18 | // Table holds the table name of the library in the database. 19 | Table = "libraries" 20 | // EventsTable is the table the holds the events relation/edge. 21 | EventsTable = "events" 22 | // EventsInverseTable is the table name for the Event entity. 23 | // It exists in this package in order to avoid circular dependency with the "event" package. 24 | EventsInverseTable = "events" 25 | // EventsColumn is the table column denoting the events relation/edge. 26 | EventsColumn = "event_library" 27 | ) 28 | 29 | // Columns holds all SQL columns for library fields. 30 | var Columns = []string{ 31 | FieldID, 32 | FieldName, 33 | FieldVersion, 34 | } 35 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/network/network.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package network 4 | 5 | const ( 6 | // Label holds the string label denoting the network type in the database. 7 | Label = "network" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldIP holds the string denoting the ip field in the database. 11 | FieldIP = "ip" 12 | // FieldUseragent holds the string denoting the useragent field in the database. 13 | FieldUseragent = "useragent" 14 | 15 | // EdgeEvents holds the string denoting the events edge name in mutations. 16 | EdgeEvents = "events" 17 | 18 | // Table holds the table name of the network in the database. 19 | Table = "networks" 20 | // EventsTable is the table the holds the events relation/edge. 21 | EventsTable = "events" 22 | // EventsInverseTable is the table name for the Event entity. 23 | // It exists in this package in order to avoid circular dependency with the "event" package. 24 | EventsInverseTable = "events" 25 | // EventsColumn is the table column denoting the events relation/edge. 26 | EventsColumn = "event_network" 27 | ) 28 | 29 | // Columns holds all SQL columns for network fields. 30 | var Columns = []string{ 31 | FieldID, 32 | FieldIP, 33 | FieldUseragent, 34 | } 35 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/oscontext/oscontext.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package oscontext 4 | 5 | const ( 6 | // Label holds the string label denoting the oscontext type in the database. 7 | Label = "os_context" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldFamily holds the string denoting the family field in the database. 13 | FieldFamily = "family" 14 | // FieldPlatform holds the string denoting the platform field in the database. 15 | FieldPlatform = "platform" 16 | // FieldVersion holds the string denoting the version field in the database. 17 | FieldVersion = "version" 18 | 19 | // EdgeEvents holds the string denoting the events edge name in mutations. 20 | EdgeEvents = "events" 21 | 22 | // Table holds the table name of the oscontext in the database. 23 | Table = "os" 24 | // EventsTable is the table the holds the events relation/edge. 25 | EventsTable = "events" 26 | // EventsInverseTable is the table name for the Event entity. 27 | // It exists in this package in order to avoid circular dependency with the "event" package. 28 | EventsInverseTable = "events" 29 | // EventsColumn is the table column denoting the events relation/edge. 30 | EventsColumn = "event_os" 31 | ) 32 | 33 | // Columns holds all SQL columns for oscontext fields. 34 | var Columns = []string{ 35 | FieldID, 36 | FieldName, 37 | FieldFamily, 38 | FieldPlatform, 39 | FieldVersion, 40 | } 41 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/page/page.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package page 4 | 5 | const ( 6 | // Label holds the string label denoting the page type in the database. 7 | Label = "page" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldHostname holds the string denoting the hostname field in the database. 11 | FieldHostname = "hostname" 12 | // FieldPath holds the string denoting the path field in the database. 13 | FieldPath = "path" 14 | // FieldReferrer holds the string denoting the referrer field in the database. 15 | FieldReferrer = "referrer" 16 | // FieldSearch holds the string denoting the search field in the database. 17 | FieldSearch = "search" 18 | // FieldTitle holds the string denoting the title field in the database. 19 | FieldTitle = "title" 20 | // FieldHash holds the string denoting the hash field in the database. 21 | FieldHash = "hash" 22 | 23 | // EdgeEvents holds the string denoting the events edge name in mutations. 24 | EdgeEvents = "events" 25 | 26 | // Table holds the table name of the page in the database. 27 | Table = "pages" 28 | // EventsTable is the table the holds the events relation/edge. 29 | EventsTable = "events" 30 | // EventsInverseTable is the table name for the Event entity. 31 | // It exists in this package in order to avoid circular dependency with the "event" package. 32 | EventsInverseTable = "events" 33 | // EventsColumn is the table column denoting the events relation/edge. 34 | EventsColumn = "event_page" 35 | ) 36 | 37 | // Columns holds all SQL columns for page fields. 38 | var Columns = []string{ 39 | FieldID, 40 | FieldHostname, 41 | FieldPath, 42 | FieldReferrer, 43 | FieldSearch, 44 | FieldTitle, 45 | FieldHash, 46 | } 47 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/referrer/referrer.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package referrer 4 | 5 | const ( 6 | // Label holds the string label denoting the referrer type in the database. 7 | Label = "referrer" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldType holds the string denoting the type field in the database. 13 | FieldType = "type" 14 | // FieldHostname holds the string denoting the hostname field in the database. 15 | FieldHostname = "hostname" 16 | // FieldLink holds the string denoting the link field in the database. 17 | FieldLink = "link" 18 | 19 | // EdgeEvents holds the string denoting the events edge name in mutations. 20 | EdgeEvents = "events" 21 | 22 | // Table holds the table name of the referrer in the database. 23 | Table = "referrers" 24 | // EventsTable is the table the holds the events relation/edge. 25 | EventsTable = "events" 26 | // EventsInverseTable is the table name for the Event entity. 27 | // It exists in this package in order to avoid circular dependency with the "event" package. 28 | EventsInverseTable = "events" 29 | // EventsColumn is the table column denoting the events relation/edge. 30 | EventsColumn = "event_referrer" 31 | ) 32 | 33 | // Columns holds all SQL columns for referrer fields. 34 | var Columns = []string{ 35 | FieldID, 36 | FieldName, 37 | FieldType, 38 | FieldHostname, 39 | FieldLink, 40 | } 41 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | // The init function reads all schema descriptors with runtime 6 | // code (default values, validators or hooks) and stitches it 7 | // to their package variables. 8 | func init() { 9 | } 10 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package runtime 4 | 5 | // The schema-stitching logic is generated in github.com/blushft/strana/modules/sink/reporter/store/ent/runtime.go 6 | 7 | const ( 8 | Version = "v0.4.1" // Version of ent codegen. 9 | Sum = "h1:UXdCoiCbsvnZYnM0j5aF1VBkdwAPCGDAJfC70a8O/tE=" // Sum of ent codegen. 10 | ) 11 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/screen/screen.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package screen 4 | 5 | const ( 6 | // Label holds the string label denoting the screen type in the database. 7 | Label = "screen" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldName holds the string denoting the name field in the database. 11 | FieldName = "name" 12 | // FieldCategory holds the string denoting the category field in the database. 13 | FieldCategory = "category" 14 | 15 | // EdgeEvents holds the string denoting the events edge name in mutations. 16 | EdgeEvents = "events" 17 | 18 | // Table holds the table name of the screen in the database. 19 | Table = "screens" 20 | // EventsTable is the table the holds the events relation/edge. 21 | EventsTable = "events" 22 | // EventsInverseTable is the table name for the Event entity. 23 | // It exists in this package in order to avoid circular dependency with the "event" package. 24 | EventsInverseTable = "events" 25 | // EventsColumn is the table column denoting the events relation/edge. 26 | EventsColumn = "event_screen" 27 | ) 28 | 29 | // Columns holds all SQL columns for screen fields. 30 | var Columns = []string{ 31 | FieldID, 32 | FieldName, 33 | FieldCategory, 34 | } 35 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/session/session.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package session 4 | 5 | const ( 6 | // Label holds the string label denoting the session type in the database. 7 | Label = "session" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldNewUser holds the string denoting the new_user field in the database. 11 | FieldNewUser = "new_user" 12 | // FieldIsUnique holds the string denoting the is_unique field in the database. 13 | FieldIsUnique = "is_unique" 14 | // FieldIsBounce holds the string denoting the is_bounce field in the database. 15 | FieldIsBounce = "is_bounce" 16 | // FieldIsFinished holds the string denoting the is_finished field in the database. 17 | FieldIsFinished = "is_finished" 18 | // FieldDuration holds the string denoting the duration field in the database. 19 | FieldDuration = "duration" 20 | // FieldStartedAt holds the string denoting the started_at field in the database. 21 | FieldStartedAt = "started_at" 22 | // FieldFinishedAt holds the string denoting the finished_at field in the database. 23 | FieldFinishedAt = "finished_at" 24 | 25 | // EdgeEvents holds the string denoting the events edge name in mutations. 26 | EdgeEvents = "events" 27 | 28 | // Table holds the table name of the session in the database. 29 | Table = "sessions" 30 | // EventsTable is the table the holds the events relation/edge. 31 | EventsTable = "events" 32 | // EventsInverseTable is the table name for the Event entity. 33 | // It exists in this package in order to avoid circular dependency with the "event" package. 34 | EventsInverseTable = "events" 35 | // EventsColumn is the table column denoting the events relation/edge. 36 | EventsColumn = "event_session" 37 | ) 38 | 39 | // Columns holds all SQL columns for session fields. 40 | var Columns = []string{ 41 | FieldID, 42 | FieldNewUser, 43 | FieldIsUnique, 44 | FieldIsBounce, 45 | FieldIsFinished, 46 | FieldDuration, 47 | FieldStartedAt, 48 | FieldFinishedAt, 49 | } 50 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/timing/timing.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package timing 4 | 5 | const ( 6 | // Label holds the string label denoting the timing type in the database. 7 | Label = "timing" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldCategory holds the string denoting the category field in the database. 11 | FieldCategory = "category" 12 | // FieldTimingLabel holds the string denoting the timing_label field in the database. 13 | FieldTimingLabel = "label" 14 | // FieldUnit holds the string denoting the unit field in the database. 15 | FieldUnit = "unit" 16 | // FieldVariable holds the string denoting the variable field in the database. 17 | FieldVariable = "variable" 18 | // FieldValue holds the string denoting the value field in the database. 19 | FieldValue = "value" 20 | 21 | // EdgeEvents holds the string denoting the events edge name in mutations. 22 | EdgeEvents = "events" 23 | 24 | // Table holds the table name of the timing in the database. 25 | Table = "timings" 26 | // EventsTable is the table the holds the events relation/edge. 27 | EventsTable = "events" 28 | // EventsInverseTable is the table name for the Event entity. 29 | // It exists in this package in order to avoid circular dependency with the "event" package. 30 | EventsInverseTable = "events" 31 | // EventsColumn is the table column denoting the events relation/edge. 32 | EventsColumn = "event_timing" 33 | ) 34 | 35 | // Columns holds all SQL columns for timing fields. 36 | var Columns = []string{ 37 | FieldID, 38 | FieldCategory, 39 | FieldTimingLabel, 40 | FieldUnit, 41 | FieldVariable, 42 | FieldValue, 43 | } 44 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/ent/viewport/viewport.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package viewport 4 | 5 | const ( 6 | // Label holds the string label denoting the viewport type in the database. 7 | Label = "viewport" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldDensity holds the string denoting the density field in the database. 11 | FieldDensity = "density" 12 | // FieldWidth holds the string denoting the width field in the database. 13 | FieldWidth = "width" 14 | // FieldHeight holds the string denoting the height field in the database. 15 | FieldHeight = "height" 16 | 17 | // EdgeEvents holds the string denoting the events edge name in mutations. 18 | EdgeEvents = "events" 19 | 20 | // Table holds the table name of the viewport in the database. 21 | Table = "viewports" 22 | // EventsTable is the table the holds the events relation/edge. 23 | EventsTable = "events" 24 | // EventsInverseTable is the table name for the Event entity. 25 | // It exists in this package in order to avoid circular dependency with the "event" package. 26 | EventsInverseTable = "events" 27 | // EventsColumn is the table column denoting the events relation/edge. 28 | EventsColumn = "event_viewport" 29 | ) 30 | 31 | // Columns holds all SQL columns for viewport fields. 32 | var Columns = []string{ 33 | FieldID, 34 | FieldDensity, 35 | FieldWidth, 36 | FieldHeight, 37 | } 38 | -------------------------------------------------------------------------------- /modules/sink/reporter/store/reporter.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/blushft/strana/modules/sink/reporter/store/ent" 8 | "github.com/blushft/strana/platform/store" 9 | 10 | entsql "github.com/facebook/ent/dialect/sql" 11 | ) 12 | 13 | type Store struct { 14 | client *ent.Client 15 | } 16 | 17 | func New(dbs *store.SQLStore) (*Store, error) { 18 | db := sql.OpenDB(dbs) 19 | drv := entsql.OpenDB(dbs.Dialect(), db) 20 | 21 | s := &Store{ 22 | client: ent.NewClient(ent.Driver(drv)), 23 | } 24 | 25 | if err := s.setup(); err != nil { 26 | return nil, err 27 | } 28 | 29 | return s, nil 30 | } 31 | 32 | func (s *Store) Client() *ent.Client { 33 | return s.client 34 | } 35 | 36 | func (s *Store) Mount(fn func(*Store)) { 37 | fn(s) 38 | } 39 | 40 | func (s *Store) setup() error { 41 | return s.client.Schema.Create(context.TODO()) 42 | } 43 | -------------------------------------------------------------------------------- /modules/source/collector/collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | 6 | "github.com/blushft/strana" 7 | "github.com/blushft/strana/modules" 8 | "github.com/blushft/strana/platform/config" 9 | "github.com/mitchellh/mapstructure" 10 | ) 11 | 12 | func init() { 13 | modules.Register("collector", New) 14 | } 15 | 16 | type Collector interface { 17 | strana.Source 18 | } 19 | 20 | type Options struct { 21 | Type string `json:"type" yaml:"type" mapstructure:"type"` 22 | Cache config.Cache `json:"cache" yaml:"cache" mapstructure:"cache"` 23 | Processors []config.Processor `json:"processors" yaml:"processors" mapstructure:"processors"` 24 | } 25 | 26 | func New(conf config.Module) (strana.Module, error) { 27 | var opts Options 28 | if err := mapstructure.Decode(conf.Options, &opts); err != nil { 29 | return nil, errors.Wrap(err, "unable to decode collector options") 30 | } 31 | 32 | switch opts.Type { 33 | case "tracker": 34 | return newTrackingCollector(conf, opts) 35 | default: 36 | return nil, errors.New("invalid collector type") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/source/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "github.com/blushft/strana" 5 | "github.com/blushft/strana/modules" 6 | "github.com/blushft/strana/platform/config" 7 | "github.com/mitchellh/mapstructure" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func init() { 12 | modules.Register("webhook", New) 13 | } 14 | 15 | type Webhook interface { 16 | strana.Source 17 | } 18 | 19 | type Options struct { 20 | Hooks map[string]HookOptions `json:"hooks" structs:"hooks" mapstructure:"hooks"` 21 | Processors []config.Processor `json:"processors" structs:"processors" mapstructure:"processors"` 22 | } 23 | 24 | type HookOptions map[string]interface{} 25 | 26 | func New(conf config.Module) (strana.Module, error) { 27 | var opts Options 28 | if err := mapstructure.Decode(conf.Options, &opts); err != nil { 29 | return nil, errors.Wrap(err, "decoding webhook options") 30 | } 31 | 32 | for t, o := range opts.Hooks { 33 | switch t { 34 | case "pingdom": 35 | return newPingdomHook(conf, o, opts.Processors...) 36 | default: 37 | return nil, errors.New("invalid webhook type") 38 | } 39 | } 40 | 41 | return nil, errors.New("invalid webhook configuration") 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "lerna": "^3.20.2" 9 | } 10 | } -------------------------------------------------------------------------------- /packages/core/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'], 7 | parserOptions: { 8 | parser: 'babel-eslint', 9 | }, 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # core 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@mdi/font": "^3.6.95", 12 | "core-js": "^3.6.5", 13 | "roboto-fontface": "*", 14 | "vue": "^2.6.11", 15 | "vue-router": "^3.2.0", 16 | "vuetify": "^2.2.11", 17 | "vuex": "^3.4.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-eslint": "~4.5.0", 22 | "@vue/cli-plugin-router": "~4.5.0", 23 | "@vue/cli-plugin-vuex": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "@vue/eslint-config-prettier": "^6.0.0", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^6.7.2", 28 | "eslint-plugin-prettier": "^3.1.3", 29 | "eslint-plugin-vue": "^6.2.2", 30 | "prettier": "^1.19.1", 31 | "sass": "^1.26.5", 32 | "sass-loader": "^8.0.2", 33 | "vue-cli-plugin-vuetify": "~2.0.7", 34 | "vue-template-compiler": "^2.6.11", 35 | "vuetify-loader": "^1.3.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blushft/strana/6edac6f1e43d696a4f0d3510d80931ea4c316adb/packages/core/public/favicon.ico -------------------------------------------------------------------------------- /packages/core/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | <%= htmlWebpackPlugin.options.title %> 13 | 14 | 15 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/core/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /packages/core/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blushft/strana/6edac6f1e43d696a4f0d3510d80931ea4c316adb/packages/core/src/assets/logo.png -------------------------------------------------------------------------------- /packages/core/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /packages/core/src/components/Admin/Header.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /packages/core/src/components/Admin/Page.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | -------------------------------------------------------------------------------- /packages/core/src/components/App/Bar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /packages/core/src/components/App/Nav.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /packages/core/src/components/Widgets/Cards/Chart.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /packages/core/src/components/Widgets/Cards/Stat.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | -------------------------------------------------------------------------------- /packages/core/src/components/Widgets/Trend.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /packages/core/src/config/app.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Strana" 3 | } -------------------------------------------------------------------------------- /packages/core/src/layouts/Admin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /packages/core/src/layouts/Page.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/core/src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import Admin from './Admin.vue' 2 | import Page from './Page.vue' 3 | 4 | export default { 5 | admin: Admin, 6 | page: Page, 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import vuetify from './plugins/vuetify' 6 | import 'roboto-fontface/css/roboto/roboto-fontface.css' 7 | import '@mdi/font/css/materialdesignicons.css' 8 | 9 | Vue.config.productionTip = false 10 | 11 | new Vue({ 12 | router, 13 | store, 14 | vuetify, 15 | render: (h) => h(App), 16 | }).$mount('#app') 17 | -------------------------------------------------------------------------------- /packages/core/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | 4 | Vue.use(Vuetify) 5 | 6 | export default new Vuetify({ 7 | theme: { 8 | dark: true, 9 | options: { 10 | customProperties: true, 11 | }, 12 | themes: { 13 | dark: { 14 | primary: '#9c27b0', 15 | secondary: '#7b1fa2', 16 | accent: '#03a8f4', 17 | divider: '#bdbdbd', 18 | lightText: '#ffffff', 19 | error: '#FF5252', 20 | info: '#2196F3', 21 | success: '#4CAF50', 22 | warning: '#FFC107', 23 | }, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Admin from '../layouts/Admin.vue' 4 | import Home from '../views/Home.vue' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Root', 12 | component: Admin, 13 | redirect: '/home', 14 | children: [ 15 | { 16 | path: '/home', 17 | name: 'Home', 18 | component: Home, 19 | }, 20 | { 21 | path: '/about', 22 | name: 'About', 23 | // route level code-splitting 24 | // this generates a separate chunk (about.[hash].js) for this route 25 | // which is lazy-loaded when the route is visited. 26 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), 27 | }, 28 | ], 29 | }, 30 | ] 31 | 32 | const router = new VueRouter({ 33 | mode: 'history', 34 | base: process.env.BASE_URL, 35 | routes, 36 | }) 37 | 38 | export default router 39 | -------------------------------------------------------------------------------- /packages/core/src/services/plugins/index.js: -------------------------------------------------------------------------------- 1 | import layouts from '../../layouts' 2 | 3 | export class PluginLoader { 4 | constructor() { 5 | this.plugins = {} 6 | } 7 | 8 | register(plugin) { 9 | this.plugins[plugin.name] = plugin 10 | } 11 | 12 | routes() { 13 | const rts = [] 14 | for (const plug in this.plugins) { 15 | rts.push(createRoute(this.plugins[plug].routes, this.plugins[plug].components)) 16 | } 17 | 18 | return rts 19 | } 20 | } 21 | 22 | function createRoute(def, comps) { 23 | const rt = { 24 | path: `/${def.name.toLower()}`, 25 | name: def.name, 26 | component: layouts[def.layout], 27 | } 28 | 29 | if (def.children.length > 0) { 30 | rt.children = [] 31 | for (const child in def.children) { 32 | rt.children.push({ 33 | path: `/${def.name.toLower()}/${child.path}`, 34 | name: child.path, 35 | component: comps[child.component], 36 | }) 37 | } 38 | } 39 | 40 | return rt 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {}, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/core/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $body-font-family: 'Kumbh Sans', sans-serif; 2 | $headings: ( 3 | 'h1': ( 4 | 'size': 18, 5 | 'line-height': 1.15em, 6 | ), 7 | ); 8 | -------------------------------------------------------------------------------- /packages/core/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/core/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | -------------------------------------------------------------------------------- /packages/core/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: ['vuetify'], 3 | } 4 | -------------------------------------------------------------------------------- /packages/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strana/plugin", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugin/src/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blushft/strana/6edac6f1e43d696a4f0d3510d80931ea4c316adb/packages/plugin/src/index.js -------------------------------------------------------------------------------- /packages/plugin/src/plugin.js: -------------------------------------------------------------------------------- 1 | export default class Plugin { 2 | constructor({ name, summary, components, routes, modules }) { 3 | this.name = name 4 | this.summary = summary 5 | 6 | this.components = components 7 | this.routes = routes 8 | this.modules = modules 9 | } 10 | 11 | mountComponents(Vue) { 12 | for (const c in this.components) { 13 | Vue.component(c) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/plugins/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strana/plugins-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT" 6 | } -------------------------------------------------------------------------------- /packages/plugins/example/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/plugins/example/src/index.js: -------------------------------------------------------------------------------- 1 | import Plugin from '@strana/plugin' 2 | import HelloWorld from './components/HelloWorld.vue' 3 | 4 | export default (options) => { 5 | const plugin = new Plugin({ 6 | name: 'example', 7 | summary: 'an example plugin', 8 | components: { 9 | HelloWorld: HelloWorld, 10 | }, 11 | routes: [ 12 | { 13 | path: '/hello', 14 | name: 'hello', 15 | component: 'HelloWorld', 16 | }, 17 | ], 18 | }) 19 | 20 | return plugin 21 | } 22 | -------------------------------------------------------------------------------- /packages/tracker/README.md: -------------------------------------------------------------------------------- 1 | # `@strana/tracker` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const tracker = require('@strana/tracker'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/tracker/__tests__/tracker.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tracker = require('..'); 4 | 5 | describe('@strana/tracker', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/tracker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@strana/tracker", 3 | "version": "0.0.0", 4 | "description": "Strana JS Analytics Tracker", 5 | "keywords": [ 6 | "analytics" 7 | ], 8 | "author": "Tom Tobias ", 9 | "homepage": "", 10 | "license": "MIT", 11 | "main": "src/tracker.js", 12 | "directories": { 13 | "src": "src", 14 | "test": "__tests__" 15 | }, 16 | "files": [ 17 | "src" 18 | ], 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "scripts": { 23 | "test": "echo \"Error: run tests from root\" && exit 1" 24 | }, 25 | "dependencies": { 26 | "@segment/localstorage-retry": "^1.2.4", 27 | "axios": "^0.19.2", 28 | "crytpo-js": "^0.0.1-security", 29 | "store": "^2.0.12" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/tracker/src/event/event.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | const validEvents = { 4 | identify: 'identify', 5 | group: 'group', 6 | session: 'session', 7 | pageview: 'pageview', 8 | screenview: 'screenview', 9 | action: 'action', 10 | } 11 | 12 | export default class Event { 13 | /** 14 | * 15 | * @param {string} event - Event type to create 16 | * @param {Object} config - Event config 17 | * 18 | */ 19 | constructor(type, config = {}) { 20 | this.eid = uuidv4() 21 | this.tid = config.trackingId || store.get(tid) 22 | 23 | this.sid = '' 24 | 25 | const storedSID = store.get(sid) 26 | if (storedSID) { 27 | this.sid = storedSID 28 | this.news = false 29 | } 30 | 31 | if (config.sessionId && config.sessionId !== this.sid) { 32 | this.sid = config.sessionId 33 | this.news = true 34 | store.set(sid, this.sid) 35 | } 36 | 37 | if (this.sid === '') { 38 | this.sid = uuidv4() 39 | this.news = true 40 | } 41 | 42 | this.uid = config.userId || store.get(uid) || '' 43 | this.gid = config.groupId || store.get(gid) || '' 44 | this.cid = config.deviceId || store.get(cid) || '' 45 | 46 | this.e = type 47 | } 48 | 49 | setTrackingId(id) { 50 | this.tid = id 51 | } 52 | 53 | setSessionId(id) { 54 | this.sid = id 55 | } 56 | 57 | setUserId(id) { 58 | this.uid = id 59 | } 60 | 61 | setGroupId(id) { 62 | this.gid = id 63 | } 64 | 65 | setDeviceId(id) { 66 | this.cid = id 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/tracker/src/index.js: -------------------------------------------------------------------------------- 1 | import Tracker from './tracker' 2 | 3 | if (typeof define !== 'undefined' && define.amd) { 4 | define(function() { 5 | 'use strict' 6 | return Tracker 7 | }) 8 | } else if (typeof module !== 'undefined' && module.exports) { 9 | module.exports = Tracker 10 | } else { 11 | window.strana = Tracker 12 | } 13 | -------------------------------------------------------------------------------- /packages/tracker/src/queue.js: -------------------------------------------------------------------------------- 1 | import Queue from '@segment/localstorage-retry' 2 | import axios from 'axios' 3 | 4 | const options = { 5 | maxRetryDelay: 360000, 6 | minRetryDelay: 1000, 7 | backoffMult: 2, 8 | maxRetryAttempts: 10, 9 | maxItems: 100, 10 | } 11 | 12 | class EventQueue { 13 | constructor(config) { 14 | this.eventBuffer = [] 15 | this.trackingId = config.trackingId 16 | this.url = config.url 17 | 18 | this.api = axios.create({ 19 | baseURL: this.url, 20 | }) 21 | 22 | this.queue = new Queue('strana', options, (item, done) => { 23 | this.processEvent() 24 | }) 25 | 26 | this.queue.start() 27 | } 28 | 29 | _setHeaders() {} 30 | 31 | processEvent() {} 32 | 33 | queue(event) { 34 | btoa 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/tracker/src/storage/index.js: -------------------------------------------------------------------------------- 1 | import store from 'store' 2 | import AES from 'crypto-js/aes' 3 | 4 | export default class Storage { 5 | constructor(options = {}) { 6 | this.valuePrefix = options.prefix || 'strana_enc_aes' 7 | this.key = options.key || 'STRANADATA' 8 | } 9 | 10 | encrypt(val) { 11 | return `${this.valuePrefix}${AES.encrypt(val, this.key).toString()}` 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue-tracker/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /packages/vue-tracker/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"], 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/vue-tracker/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /packages/vue-tracker/README.md: -------------------------------------------------------------------------------- 1 | # vue-tracker 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /packages/vue-tracker/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /packages/vue-tracker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-tracker", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.4.0", 16 | "@vue/cli-plugin-eslint": "~4.4.0", 17 | "@vue/cli-service": "~4.4.0", 18 | "@vue/eslint-config-prettier": "^6.0.0", 19 | "babel-eslint": "^10.1.0", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-prettier": "^3.1.3", 22 | "eslint-plugin-vue": "^6.2.2", 23 | "prettier": "^1.19.1", 24 | "vue-template-compiler": "^2.6.11" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vue-tracker/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blushft/strana/6edac6f1e43d696a4f0d3510d80931ea4c316adb/packages/vue-tracker/public/favicon.ico -------------------------------------------------------------------------------- /packages/vue-tracker/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/vue-tracker/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /packages/vue-tracker/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blushft/strana/6edac6f1e43d696a4f0d3510d80931ea4c316adb/packages/vue-tracker/src/assets/logo.png -------------------------------------------------------------------------------- /packages/vue-tracker/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount("#app"); 9 | -------------------------------------------------------------------------------- /pkg/conv/string.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func ToString(v interface{}) string { 9 | sr, ok := v.(fmt.Stringer) 10 | if ok { 11 | return sr.String() 12 | } 13 | 14 | switch s := v.(type) { 15 | case string: 16 | return s 17 | case int: 18 | return strconv.Itoa(s) 19 | } 20 | 21 | return "" 22 | } 23 | -------------------------------------------------------------------------------- /pkg/netintf/netintf.go: -------------------------------------------------------------------------------- 1 | package netintf 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func PrivateCIDRs() []*net.IPNet { 10 | pbs := []string{ 11 | "10.0.0.0/8", 12 | "172.16.0.0/12", 13 | "192.168.0.0/16", 14 | "100.64.0.0/10", 15 | "fd00::/8", 16 | } 17 | 18 | res := make([]*net.IPNet, 0, len(pbs)) 19 | for _, b := range pbs { 20 | if _, block, err := net.ParseCIDR(b); err == nil { 21 | res = append(res, block) 22 | } 23 | } 24 | 25 | return res 26 | } 27 | 28 | func IsPrivate(addr net.IP) bool { 29 | for _, priv := range PrivateCIDRs() { 30 | if priv.Contains(addr) { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } 37 | 38 | func AvailableAddresses() ([]string, error) { 39 | ifaces, err := net.Interfaces() 40 | if err != nil { 41 | return nil, errors.Wrap(err, "failed to list interfaces") 42 | } 43 | 44 | var addrs []string 45 | for _, iface := range ifaces { 46 | iaddrs, err := iface.Addrs() 47 | if err != nil { 48 | continue 49 | } 50 | 51 | for _, ia := range iaddrs { 52 | var ip net.IP 53 | 54 | switch v := ia.(type) { 55 | case *net.IPNet: 56 | ip = v.IP 57 | case *net.IPAddr: 58 | ip = v.IP 59 | default: 60 | continue 61 | } 62 | 63 | if ip == nil { 64 | continue 65 | } 66 | 67 | addrs = append(addrs, ip.String()) 68 | } 69 | } 70 | 71 | return addrs, nil 72 | } 73 | -------------------------------------------------------------------------------- /platform/bus/broker/broker.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import "github.com/blushft/strana" 4 | 5 | type Broker interface { 6 | Publisher() strana.Publisher 7 | Subscriber() strana.Subscriber 8 | Connect() error 9 | Disconnect() error 10 | } 11 | -------------------------------------------------------------------------------- /platform/bus/broker/nats/options.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/mitchellh/mapstructure" 6 | ) 7 | 8 | type Options struct { 9 | Port int `json:"port" structs:"port" mapstructure:"port"` 10 | HTTPPort int `json:"httpPort" structs:"httpPort" mapstructure:"httpPort"` 11 | Token string `json:"token" structs:"token" mapstructure:"token"` 12 | } 13 | 14 | func newOptions() Options { 15 | return Options{ 16 | Port: 4442, 17 | HTTPPort: 4443, 18 | Token: uuid.New().String(), 19 | } 20 | } 21 | 22 | func unmarshalOptions(m map[string]interface{}) (*Options, error) { 23 | options := newOptions() 24 | if err := mapstructure.Decode(m, &options); err != nil { 25 | return nil, err 26 | } 27 | 28 | return &options, nil 29 | } 30 | -------------------------------------------------------------------------------- /platform/bus/broker/nsq/nsq.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "github.com/blushft/strana/platform/bus/broker" 5 | ) 6 | 7 | func NewDefault(opts ...broker.Option) broker.Broker { 8 | b, err := New(opts...) 9 | if err != nil { 10 | panic(err) 11 | } 12 | 13 | return b 14 | } 15 | 16 | func New(opts ...broker.Option) (broker.Broker, error) { 17 | options := broker.NewOptions(opts...) 18 | 19 | if options.Embedded { 20 | return newEmbedded(newOptions(options)) 21 | } 22 | 23 | return nil, nil 24 | } 25 | -------------------------------------------------------------------------------- /platform/bus/broker/nsq/options.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "github.com/blushft/strana/platform/bus/broker" 5 | "github.com/blushft/strana/platform/logger" 6 | "github.com/nsqio/nsq/nsqd" 7 | ) 8 | 9 | type Options struct { 10 | BrokerOptions broker.Options 11 | NSQOptions *nsqd.Options 12 | } 13 | 14 | func newOptions(opts broker.Options) Options { 15 | return Options{ 16 | BrokerOptions: opts, 17 | NSQOptions: nsqOptions(), 18 | } 19 | } 20 | 21 | func nsqOptions() *nsqd.Options { 22 | nopts := nsqd.NewOptions() 23 | 24 | nopts.Logger = nsqLogger{ 25 | logger.Log(), 26 | } 27 | 28 | return nopts 29 | } 30 | 31 | type nsqLogger struct { 32 | log *logger.Logger 33 | } 34 | 35 | func (nl nsqLogger) Output(md int, msg string) error { 36 | nl.log.Info(msg) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /platform/bus/broker/options.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | type Options struct { 4 | Embedded bool 5 | Web bool 6 | } 7 | 8 | type Option func(*Options) 9 | 10 | func NewOptions(opts ...Option) Options { 11 | options := Options{ 12 | Embedded: true, 13 | Web: true, 14 | } 15 | 16 | for _, o := range opts { 17 | o(&options) 18 | } 19 | 20 | return options 21 | } 22 | -------------------------------------------------------------------------------- /platform/bus/message/envelope.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Marshaler func(*Message) (Envelope, error) 8 | 9 | var marshaler = func(msg *Message) (Envelope, error) { 10 | return json.Marshal(msg) 11 | } 12 | 13 | func SetMarshaler(fn Marshaler) { 14 | marshaler = fn 15 | } 16 | 17 | type Unmarshaler func([]byte) (*Message, error) 18 | 19 | var unmarshaler = func(b []byte) (*Message, error) { 20 | var msg *Message 21 | if err := json.Unmarshal(b, &msg); err != nil { 22 | return nil, err 23 | } 24 | 25 | return msg, nil 26 | } 27 | 28 | func SetUnmarshaler(fn Unmarshaler) { 29 | unmarshaler = fn 30 | } 31 | 32 | type Envelope []byte 33 | 34 | func NewEnvelope(msg *Message) (Envelope, error) { 35 | return marshaler(msg) 36 | } 37 | 38 | func (e Envelope) Copy() Envelope { 39 | var ec Envelope 40 | copy(ec, e) 41 | 42 | return ec 43 | } 44 | 45 | func (e Envelope) Unmarshal() (*Message, error) { 46 | return unmarshaler(e) 47 | } 48 | -------------------------------------------------------------------------------- /platform/bus/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type UUIDGenerator func() uuid.UUID 9 | 10 | var uuidGenerator = func() uuid.UUID { 11 | return uuid.New() 12 | } 13 | 14 | func SetUUIDGenerator(fn UUIDGenerator) { 15 | uuidGenerator = fn 16 | } 17 | 18 | type Path struct { 19 | Topic string 20 | Channel string 21 | Broker string 22 | } 23 | 24 | type Message struct { 25 | ID uuid.UUID 26 | Metadata map[string]string 27 | Event *event.Event 28 | 29 | state string 30 | } 31 | 32 | type MessageOption func(*Message) 33 | 34 | func WithMetadata(k, v string) MessageOption { 35 | return func(m *Message) { 36 | m.Metadata[k] = v 37 | } 38 | } 39 | 40 | func SetMetadata(meta map[string]string) MessageOption { 41 | return func(m *Message) { 42 | m.Metadata = meta 43 | } 44 | } 45 | 46 | func NewMessage(evt *event.Event, opts ...MessageOption) *Message { 47 | msg := &Message{ 48 | ID: uuidGenerator(), 49 | Metadata: make(map[string]string), 50 | Event: evt, 51 | } 52 | 53 | for _, o := range opts { 54 | o(msg) 55 | } 56 | 57 | return msg 58 | } 59 | 60 | func (msg *Message) Envelope() (Envelope, error) { 61 | return NewEnvelope(msg) 62 | } 63 | -------------------------------------------------------------------------------- /platform/bus/options.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | type Options struct { 4 | Embedded bool 5 | } 6 | 7 | type Option func(*Options) 8 | 9 | func NewOptions(opts ...Option) Options { 10 | options := Options{ 11 | Embedded: true, 12 | } 13 | 14 | for _, o := range opts { 15 | o(&options) 16 | } 17 | 18 | return options 19 | } 20 | 21 | func Embedded(b bool) Option { 22 | return func(o *Options) { 23 | o.Embedded = b 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /platform/bus/route.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "github.com/blushft/strana" 5 | "github.com/blushft/strana/platform/bus/message" 6 | ) 7 | 8 | type Route struct { 9 | src message.Path 10 | pub strana.Publisher 11 | 12 | sink message.Path 13 | sub strana.Subscriber 14 | 15 | hndlr strana.EventHandlerFunc 16 | } 17 | 18 | func NewRoute(src, sink message.Path, bus Bus, hndlr strana.EventHandlerFunc) (*Route, error) { 19 | r := &Route{ 20 | src: src, 21 | sink: sink, 22 | hndlr: hndlr, 23 | } 24 | 25 | return r, nil 26 | } 27 | 28 | /* func (r *route) handleSub(msg *nats.Msg) { 29 | e := message.Envelope(msg.Data) 30 | m, err := e.Unmarshal() 31 | if err != nil { 32 | log.Printf("unable to unmarshal event envelope: %v", err) 33 | } 34 | 35 | msgs, err := r.hndlr(m) 36 | if err != nil { 37 | log.Printf("error handling event for subscription: %s, %v", r.source, err) 38 | return 39 | } 40 | 41 | for _, om := range msgs { 42 | oe, err := om.Envelope() 43 | if err != nil { 44 | log.Printf("error marshaling event enveleope: %v", err) 45 | continue 46 | } 47 | 48 | if err := r.conn.Publish(r.sink, oe); err != nil { 49 | log.Printf("error publishing result to %s: %v", r.sink, err) 50 | continue 51 | } 52 | } 53 | } */ 54 | 55 | func (r *Route) Close() error { 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /platform/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/allegro/bigcache" 7 | "github.com/blushft/strana/platform/config" 8 | "github.com/eko/gocache/cache" 9 | "github.com/eko/gocache/store" 10 | ) 11 | 12 | type Cache struct { 13 | *cache.Cache 14 | } 15 | 16 | func NewCache(conf config.Cache) (*Cache, error) { 17 | if conf.DefaultExpiration == 0 { 18 | conf.DefaultExpiration = 15 19 | } 20 | 21 | de := time.Duration(conf.DefaultExpiration) 22 | bcc, err := bigcache.NewBigCache(bigcache.DefaultConfig(de * time.Minute)) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | bsc := store.NewBigcache(bcc, nil) 28 | 29 | return &Cache{ 30 | Cache: cache.New(bsc), 31 | }, nil 32 | } 33 | -------------------------------------------------------------------------------- /platform/config/bus.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Bus struct { 4 | Debug bool `json:"debug" yaml:"debug" mapstructure:"debug" structs:"debug"` 5 | Trace bool `json:"trace" yaml:"trace" mapstructure:"trace" structs:"trace"` 6 | Brokers []Broker `json:"broker" structs:"broker" mapstructure:"broker"` 7 | Options map[string]interface{} `json:"options" structs:"options" mapstructure:"options"` 8 | } 9 | 10 | func DefaultBusConfig() Bus { 11 | return Bus{ 12 | Debug: false, 13 | Trace: false, 14 | Brokers: []Broker{ 15 | { 16 | Name: "nsq", 17 | Type: "nsq", 18 | Options: map[string]interface{}{ 19 | "embedded": true, 20 | }, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | type Broker struct { 27 | Name string `json:"name" structs:"name" mapstructure:"name"` 28 | Type string `json:"type" structs:"type" mapstructure:"type"` 29 | Options map[string]interface{} `json:"options" structs:"options" mapstructure:"options"` 30 | } 31 | -------------------------------------------------------------------------------- /platform/config/cache.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Cache struct { 4 | Store string `json:"store" yaml:"store" mapstructure:"store"` 5 | DefaultExpiration int `json:"default_expiration" yaml:"default_expiration" mapstructure:"default_expiration"` 6 | } 7 | 8 | func DefaultCacheConfig() Cache { 9 | return Cache{ 10 | Store: "memory", 11 | DefaultExpiration: 15, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /platform/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | type Config struct { 8 | Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"` 9 | Logger Logger `json:"logger" yaml:"logger" mapstructure:"logger"` 10 | Database Database `json:"database" yaml:"database" mapstructure:"database"` 11 | Bus Bus `json:"bus" yaml:"bus" mapstructure:"bus"` 12 | Cache Cache `json:"cache" yaml:"cache" mapstructure:"cache"` 13 | Server Server `json:"server" yaml:"server" mapstructure:"server"` 14 | Modules []Module `json:"modules" yaml:"modules" mapstructure:"modules"` 15 | } 16 | 17 | func NewConfig(v *viper.Viper) (*Config, error) { 18 | conf := DefaultConfig() 19 | if err := v.Unmarshal(&conf); err != nil { 20 | return nil, err 21 | } 22 | 23 | return &conf, nil 24 | } 25 | 26 | func DefaultConfig() Config { 27 | return Config{ 28 | Debug: false, 29 | Logger: DefaultLoggerConfig(), 30 | Database: DefaultDatabaseConfig(), 31 | Bus: DefaultBusConfig(), 32 | Cache: DefaultCacheConfig(), 33 | Server: DefaultServerConfig(), 34 | Modules: DefaultModuleConfig(), 35 | } 36 | } 37 | 38 | func Init() { 39 | v := viper.GetViper() 40 | 41 | v.SetDefault("database.dialect", "sqlite") 42 | v.SetDefault("database.database", "strana.db") 43 | } 44 | -------------------------------------------------------------------------------- /platform/config/file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type FileStore struct { 4 | Provider string `json:"provider" yaml:"provider"` 5 | Options map[string]interface{} `json:"options" yaml:"options"` 6 | } 7 | -------------------------------------------------------------------------------- /platform/config/logger.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Logger struct { 4 | Outputs map[string]Output `json:"outputs" yaml:"outputs" mapstructure:"outputs"` 5 | } 6 | 7 | type Output struct { 8 | Type string `json:"type" yaml:"type" mapstructure:"type"` 9 | Options map[string]interface{} `json:"options" yaml:"options" mapstructure:"options"` 10 | } 11 | 12 | func DefaultLoggerConfig() Logger { 13 | return Logger{ 14 | Outputs: map[string]Output{ 15 | "console": {Type: "text"}, 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /platform/config/module.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/blushft/strana/platform/bus/message" 4 | 5 | type Module struct { 6 | Name string `json:"name" yaml:"name" mapstructure:"name"` 7 | Type string `json:"type" yaml:"type" mapstructure:"type"` 8 | Source message.Path `json:"source" yaml:"source" mapstructure:"source"` 9 | Sink message.Path `json:"sink" yaml:"sink" mapstructure:"sink"` 10 | Options map[string]interface{} `json:"options" yaml:"options" mapstructure:"options"` 11 | } 12 | 13 | func DefaultModuleConfig() []Module { 14 | return []Module{ 15 | { 16 | Name: "collector", 17 | Type: "collector", 18 | Sink: message.Path{ 19 | Broker: "nsq", 20 | Topic: "collected_raw_message", 21 | }, 22 | Options: map[string]interface{}{ 23 | "type": "tracker", 24 | "cache": map[string]interface{}{ 25 | "default_expiration": 15, 26 | }, 27 | "processors": []map[string]interface{}{ 28 | {"name": "ua", "type": "useragent"}, 29 | }, 30 | }, 31 | }, 32 | { 33 | Name: "webhook", 34 | Type: "webhook", 35 | Sink: message.Path{ 36 | Broker: "nsq", 37 | Topic: "collected_raw_message", 38 | }, 39 | Options: map[string]interface{}{ 40 | "hooks": map[string]interface{}{ 41 | "pingdom": map[string]interface{}{ 42 | "path": "/webhook/pingdom", 43 | "trackingID": "abc123", 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | Name: "reporter", 50 | Type: "reporter", 51 | Source: message.Path{ 52 | Broker: "nsq", 53 | Channel: "reporter", 54 | Topic: "collected_raw_message", 55 | }, 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /platform/config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | func BindOptions(m map[string]interface{}, v interface{}) error { 6 | cfg := &mapstructure.DecoderConfig{ 7 | DecodeHook: mapstructure.StringToSliceHookFunc(","), 8 | WeaklyTypedInput: true, 9 | Result: v, 10 | } 11 | 12 | dec, err := mapstructure.NewDecoder(cfg) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | return dec.Decode(m) 18 | } 19 | -------------------------------------------------------------------------------- /platform/config/processor.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Processor struct { 4 | Name string 5 | Type string 6 | Options map[string]interface{} 7 | } 8 | -------------------------------------------------------------------------------- /platform/config/server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type Server struct { 10 | Host string `json:"host" yaml:"host" mapstructure:"host" yaml.mapstructure:"host"` 11 | Port string `json:"port" yaml:"port" mapstructure:"port" yaml.mapstructure:"port"` 12 | EnableMetrics bool `json:"enable_metrics" yaml:"enable_metrics" mapstructure:"enable_metrics" yaml.mapstructure:"enable_metrics"` 13 | Health ServerHealth `json:"health" yaml:"health" mapstructure:"health"` 14 | } 15 | 16 | type ServerHealth struct { 17 | Enabled bool `json:"enabled" yaml:"enabled" mapstructure:"enabled"` 18 | Path string `json:"path" yaml:"path" mapstructure:"path"` 19 | Listener string `json:"listener" yaml:"listener" mapstructure:"listener"` 20 | } 21 | 22 | func DefaultServerConfig() Server { 23 | v := viper.GetViper() 24 | return Server{ 25 | Host: v.GetString("server.host"), 26 | Port: v.GetString("server.port"), 27 | Health: ServerHealth{ 28 | Enabled: true, 29 | Path: "/healthz", 30 | }, 31 | EnableMetrics: false, 32 | } 33 | } 34 | 35 | func (c Server) HostPort() string { 36 | return fmt.Sprintf("%s:%s", c.Host, c.Port) 37 | } 38 | -------------------------------------------------------------------------------- /platform/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/apex/log" 7 | "github.com/apex/log/handlers/cli" 8 | "github.com/apex/log/handlers/json" 9 | "github.com/apex/log/handlers/multi" 10 | "github.com/apex/log/handlers/text" 11 | "github.com/blushft/strana/platform/config" 12 | ) 13 | 14 | var _global *Logger 15 | 16 | func init() { 17 | Init(config.DefaultLoggerConfig()) 18 | _global = New() 19 | } 20 | 21 | func Log() *Logger { 22 | return _global 23 | } 24 | 25 | func WithFields(f Fields) *Logger { 26 | return _global.WithFields(f) 27 | } 28 | 29 | type Fields map[string]interface{} 30 | 31 | func (f Fields) Fields() log.Fields { 32 | return log.Fields(f) 33 | } 34 | 35 | type Logger struct { 36 | log.Interface 37 | } 38 | 39 | func Init(conf config.Logger) { 40 | configure(conf) 41 | } 42 | 43 | func New() *Logger { 44 | return &Logger{ 45 | Interface: log.Log, 46 | } 47 | } 48 | 49 | func Copy(l log.Interface) *Logger { 50 | return &Logger{ 51 | Interface: l, 52 | } 53 | } 54 | 55 | func (l *Logger) WithFields(f log.Fielder) *Logger { 56 | return Copy(l.Interface.WithFields(f)) 57 | } 58 | 59 | func (l *Logger) Mount(fn func(*Logger)) { 60 | fn(l) 61 | } 62 | 63 | func configure(c config.Logger) { 64 | hdlrs := handlers(c) 65 | if len(hdlrs) > 1 { 66 | log.SetHandler(multi.New(hdlrs...)) 67 | 68 | return 69 | } 70 | 71 | log.SetLevel(log.DebugLevel) 72 | log.SetHandler(hdlrs[0]) 73 | } 74 | 75 | func handlers(c config.Logger) []log.Handler { 76 | hdlrs := make([]log.Handler, 0, len(c.Outputs)) 77 | for _, o := range c.Outputs { 78 | hdlrs = append(hdlrs, configHandler(o)) 79 | } 80 | 81 | return hdlrs 82 | } 83 | 84 | func configHandler(c config.Output) log.Handler { 85 | switch c.Type { 86 | case "text": 87 | return text.New(os.Stderr) 88 | case "cli": 89 | return cli.New(os.Stderr) 90 | case "json": 91 | return json.New(os.Stderr) 92 | default: 93 | return text.New(os.Stderr) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /platform/platform.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/blushft/strana/processor" 8 | 9 | "github.com/blushft/strana/platform/bus/broker" 10 | "github.com/blushft/strana/platform/bus/broker/nsq" 11 | "github.com/blushft/strana/platform/config" 12 | ) 13 | 14 | var ErrInvalidBroker = errors.New("invalid broker") 15 | 16 | var ( 17 | Brokers = map[string]func(...broker.Option) broker.Broker{ 18 | "nsq": nsq.NewDefault, 19 | } 20 | ) 21 | 22 | func NewBroker(conf config.Broker) (broker.Broker, error) { 23 | switch conf.Type { 24 | case "nsq": 25 | return nsq.New() 26 | default: 27 | return nil, ErrInvalidBroker 28 | } 29 | } 30 | 31 | var ( 32 | _procRegistry = &procRegistry{reg: make(map[string]processor.Constructor)} 33 | ) 34 | 35 | func RegisterEventProcessor(name string, proc processor.Constructor) { 36 | _procRegistry.mu.Lock() 37 | _procRegistry.reg[name] = proc 38 | _procRegistry.mu.Unlock() 39 | } 40 | 41 | func NewEventProcessor(conf config.Processor) (processor.EventProcessor, error) { 42 | return _procRegistry.new(conf) 43 | } 44 | 45 | func NewEventProcessorSet(conf []config.Processor) ([]processor.EventProcessor, error) { 46 | procs := make([]processor.EventProcessor, 0, len(conf)) 47 | for _, p := range conf { 48 | proc, err := NewEventProcessor(p) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | procs = append(procs, proc) 54 | } 55 | 56 | return procs, nil 57 | } 58 | 59 | type procRegistry struct { 60 | mu sync.Mutex 61 | reg map[string]processor.Constructor 62 | } 63 | 64 | func (reg *procRegistry) new(conf config.Processor) (processor.EventProcessor, error) { 65 | reg.mu.Lock() 66 | defer reg.mu.Unlock() 67 | 68 | p, ok := reg.reg[conf.Type] 69 | if !ok { 70 | return nil, errors.New("no processor found for " + conf.Type) 71 | } 72 | 73 | return p(conf) 74 | } 75 | -------------------------------------------------------------------------------- /platform/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/blushft/strana/platform/config" 5 | "github.com/blushft/strana/platform/logger" 6 | "github.com/gofiber/fiber" 7 | "github.com/gofiber/fiber/middleware" 8 | ) 9 | 10 | type Server struct { 11 | conf config.Server 12 | app *fiber.App 13 | } 14 | 15 | func New(conf config.Server, debug ...bool) *Server { 16 | isDebug := len(debug) > 0 && debug[0] 17 | 18 | app := fiber.New(&fiber.Settings{ 19 | DisableStartupMessage: false, 20 | }) 21 | 22 | app.Use(middleware.Logger()) 23 | 24 | if !isDebug { 25 | app.Use(middleware.Recover()) 26 | } else { 27 | logger.Log().Info("debugging enabled") 28 | } 29 | 30 | app.Get("/healthz", func(c *fiber.Ctx) { 31 | c.Status(200).Send("OK") 32 | }) 33 | 34 | return &Server{ 35 | conf: conf, 36 | app: app, 37 | } 38 | } 39 | 40 | func (s *Server) Mount(fn func(fiber.Router) error) error { 41 | return fn(s.app) 42 | } 43 | 44 | func (s *Server) Router() fiber.Router { 45 | return s.app 46 | } 47 | 48 | func (s *Server) Start() error { 49 | return s.app.Listen(s.conf.HostPort()) 50 | } 51 | 52 | func (s *Server) Shutdown() error { 53 | return s.app.Shutdown() 54 | } 55 | -------------------------------------------------------------------------------- /platform/store/file..go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "os" 4 | 5 | type FileStore interface { 6 | Save(os.File) error 7 | Open(string, *os.File) error 8 | List() error 9 | } 10 | -------------------------------------------------------------------------------- /platform/store/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/minio/minio-go/v6" 7 | ) 8 | 9 | type Options struct { 10 | Bucket string `json:"bucket" yaml:"bucket"` 11 | Location string `json:"location" yaml:"location"` 12 | Prefix string `json:"prefix" yaml:"prefix"` 13 | URL string `json:"url" yaml:"url"` 14 | Access string `json:"access" yaml:"access"` 15 | Secret string `json:"secret" yaml:"secret"` 16 | Secure bool `json:"secure" yaml:"secure"` 17 | } 18 | 19 | type Option func(*Options) 20 | 21 | type minioStore struct { 22 | opts Options 23 | } 24 | 25 | func (s *minioStore) client() (*minio.Client, error) { 26 | return minio.New(s.opts.URL, s.opts.Access, s.opts.Secret, s.opts.Secure) 27 | } 28 | 29 | func (s *minioStore) Save(f *os.File) error { 30 | mc, err := s.client() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | if err := mc.MakeBucket(s.opts.Bucket, s.opts.Location); err != nil { 36 | ex, xerr := mc.BucketExists(s.opts.Bucket) 37 | if xerr != nil { 38 | return xerr 39 | } 40 | 41 | if !ex { 42 | return err 43 | } 44 | } 45 | 46 | _, err = mc.FPutObject(s.opts.Bucket, f.Name(), f.Name(), minio.PutObjectOptions{}) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (s *minioStore) Open(p string, f *os.File) error { 55 | mc, err := s.client() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return mc.FGetObject(s.opts.Bucket, p, f.Name(), minio.GetObjectOptions{}) 61 | } 62 | -------------------------------------------------------------------------------- /platform/store/sql.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | 7 | "contrib.go.opencensus.io/integrations/ocsql" 8 | "github.com/blushft/strana/platform/config" 9 | 10 | _ "github.com/jackc/pgx/v4/stdlib" 11 | _ "github.com/lib/pq" 12 | _ "github.com/mattn/go-sqlite3" 13 | ) 14 | 15 | type SQLStore struct { 16 | conf config.Database 17 | } 18 | 19 | func NewSQL(conf config.Database) (*SQLStore, error) { 20 | return &SQLStore{ 21 | conf: conf, 22 | }, nil 23 | } 24 | 25 | func (s *SQLStore) Dialect() string { 26 | return s.conf.EntDialect() 27 | } 28 | 29 | func (s *SQLStore) Connect(context.Context) (driver.Conn, error) { 30 | return s.Driver().Open(s.conf.ConnString()) 31 | } 32 | 33 | func (s *SQLStore) Driver() driver.Driver { 34 | return ocsql.Wrap( 35 | s.conf.Driver(), 36 | ocsql.WithAllTraceOptions(), 37 | ocsql.WithRowsClose(false), 38 | ocsql.WithRowsNext(false), 39 | ocsql.WithDisableErrSkip(true), 40 | ) 41 | } 42 | 43 | func (s *SQLStore) Mount(fn func(*SQLStore) error) error { 44 | return fn(s) 45 | } 46 | -------------------------------------------------------------------------------- /processor/filter/condition.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/blushft/strana/event" 4 | 5 | type Condition interface { 6 | Name() string 7 | Check(*event.Event) bool 8 | } 9 | 10 | type Conditions []Condition 11 | 12 | func (cs Conditions) Add(cond Condition) { 13 | cs = append(cs, cond) 14 | } 15 | -------------------------------------------------------------------------------- /processor/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/blushft/strana/event" 4 | 5 | type filter struct { 6 | conds Conditions 7 | } 8 | 9 | func (f *filter) Process(evt *event.Event) ([]*event.Event, error) { 10 | for _, c := range f.conds { 11 | if c.Check(evt) { 12 | return nil, nil 13 | } 14 | } 15 | 16 | return []*event.Event{evt}, nil 17 | } 18 | -------------------------------------------------------------------------------- /processor/geoip/geoip_test.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/blushft/strana/platform/config" 8 | ) 9 | 10 | func Test_new(t *testing.T) { 11 | license := os.Getenv("STRANA_MAX_MIND_LICENSE") 12 | if license == "" { 13 | return 14 | } 15 | 16 | type args struct { 17 | conf config.Processor 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | wantErr bool 23 | }{ 24 | { 25 | name: "test_auto_dl", 26 | args: args{ 27 | conf: config.Processor{ 28 | Name: "geoip", 29 | Type: "geoip", 30 | Options: map[string]interface{}{ 31 | "database_path": "../../.fixtures/geoip", 32 | "max_mind_license": license, 33 | }, 34 | }, 35 | }, 36 | wantErr: true, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | _, err := new(tt.args.conf) 42 | if (err != nil) != tt.wantErr { 43 | t.Errorf("new() error = %v, wantErr %v", err, tt.wantErr) 44 | return 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /processor/init/init.go: -------------------------------------------------------------------------------- 1 | package init 2 | 3 | import ( 4 | // Registration side effects 5 | _ "github.com/blushft/strana/processor/geoip" 6 | _ "github.com/blushft/strana/processor/log" 7 | _ "github.com/blushft/strana/processor/useragent" 8 | ) 9 | -------------------------------------------------------------------------------- /processor/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/platform" 8 | "github.com/blushft/strana/platform/config" 9 | "github.com/blushft/strana/platform/logger" 10 | "github.com/blushft/strana/processor" 11 | ) 12 | 13 | func init() { 14 | platform.RegisterEventProcessor("log", func(config.Processor) (processor.EventProcessor, error) { 15 | return &loggerp{}, nil 16 | }) 17 | } 18 | 19 | type loggerp struct{} 20 | 21 | func (l *loggerp) Process(evt *event.Event) ([]*event.Event, error) { 22 | b, err := json.MarshalIndent(evt, " ", " ") 23 | if err != nil { 24 | logger.Log().WithError(err).Error("unable to marshal event") 25 | } else { 26 | logger.Log().Info(string(b)) 27 | } 28 | return []*event.Event{evt}, nil 29 | } 30 | -------------------------------------------------------------------------------- /processor/plugin/example/.strana.yaml: -------------------------------------------------------------------------------- 1 | name: simple 2 | description: Simple example plugin 3 | type: processor 4 | import: github.com/blushft/strana/processor/plugin/example 5 | -------------------------------------------------------------------------------- /processor/plugin/example/example.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/processor" 8 | ) 9 | 10 | type Config struct { 11 | Name string 12 | } 13 | 14 | type SimplePlugin struct { 15 | config *Config 16 | count int 17 | } 18 | 19 | func NewConfig() *Config { 20 | return &Config{} 21 | } 22 | 23 | func New(c *Config) processor.EventProcessor { 24 | return &SimplePlugin{config: c} 25 | } 26 | 27 | func (p *SimplePlugin) Process(evt *event.Event) ([]*event.Event, error) { 28 | p.count++ 29 | fmt.Printf("plugin %s, event count: %d\n", p.config.Name, p.count) 30 | 31 | var res []*event.Event 32 | 33 | res = append(res, evt) 34 | 35 | return res, nil 36 | } 37 | -------------------------------------------------------------------------------- /processor/plugin/imports/github.com_blushft_strana_event_events.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'github.com/containous/yaegi/extract github.com/blushft/strana/event/events'. DO NOT EDIT. 2 | 3 | // +build go1.14,!go1.15 4 | 5 | package imports 6 | 7 | import ( 8 | "github.com/blushft/strana/event/events" 9 | "reflect" 10 | ) 11 | 12 | func init() { 13 | Symbols["github.com/blushft/strana/event/events"] = map[string]reflect.Value{ 14 | // function, constant and variable definitions 15 | "Action": reflect.ValueOf(events.Action), 16 | "Alias": reflect.ValueOf(events.Alias), 17 | "EventTypeAction": reflect.ValueOf(events.EventTypeAction), 18 | "EventTypeAlias": reflect.ValueOf(events.EventTypeAlias), 19 | "EventTypeGroup": reflect.ValueOf(events.EventTypeGroup), 20 | "EventTypeIdentify": reflect.ValueOf(events.EventTypeIdentify), 21 | "EventTypePageview": reflect.ValueOf(events.EventTypePageview), 22 | "EventTypeScreen": reflect.ValueOf(events.EventTypeScreen), 23 | "EventTypeSession": reflect.ValueOf(events.EventTypeSession), 24 | "EventTypeTiming": reflect.ValueOf(events.EventTypeTiming), 25 | "EventTypeTransaction": reflect.ValueOf(events.EventTypeTransaction), 26 | "Group": reflect.ValueOf(events.Group), 27 | "Identify": reflect.ValueOf(events.Identify), 28 | "Pageview": reflect.ValueOf(events.Pageview), 29 | "Screen": reflect.ValueOf(events.Screen), 30 | "Session": reflect.ValueOf(events.Session), 31 | "Timing": reflect.ValueOf(events.Timing), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /processor/plugin/imports/github.com_blushft_strana_processor.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'github.com/containous/yaegi/extract github.com/blushft/strana/processor'. DO NOT EDIT. 2 | 3 | // +build go1.14,!go1.15 4 | 5 | package imports 6 | 7 | import ( 8 | "github.com/blushft/strana/event" 9 | "github.com/blushft/strana/processor" 10 | "reflect" 11 | ) 12 | 13 | func init() { 14 | Symbols["github.com/blushft/strana/processor"] = map[string]reflect.Value{ 15 | // function, constant and variable definitions 16 | "Execute": reflect.ValueOf(processor.Execute), 17 | 18 | // type definitions 19 | "Constructor": reflect.ValueOf((*processor.Constructor)(nil)), 20 | "EventProcessor": reflect.ValueOf((*processor.EventProcessor)(nil)), 21 | "ProcessFunc": reflect.ValueOf((*processor.ProcessFunc)(nil)), 22 | "ProcessorWrapper": reflect.ValueOf((*processor.ProcessorWrapper)(nil)), 23 | 24 | // interface wrapper definitions 25 | "_EventProcessor": reflect.ValueOf((*_github_com_blushft_strana_processor_EventProcessor)(nil)), 26 | } 27 | } 28 | 29 | // _github_com_blushft_strana_processor_EventProcessor is an interface wrapper for EventProcessor type 30 | type _github_com_blushft_strana_processor_EventProcessor struct { 31 | WProcess func(a0 *event.Event) ([]*event.Event, error) 32 | } 33 | 34 | func (W _github_com_blushft_strana_processor_EventProcessor) Process(a0 *event.Event) ([]*event.Event, error) { 35 | return W.WProcess(a0) 36 | } 37 | -------------------------------------------------------------------------------- /processor/plugin/imports/imports.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import "reflect" 4 | 5 | var Symbols = map[string]map[string]reflect.Value{} 6 | 7 | //go:generate yaegi extract github.com/blushft/strana/event 8 | //go:generate yaegi extract github.com/blushft/strana/event/contexts 9 | //go:generate yaegi extract github.com/blushft/strana/event/events 10 | //go:generate yaegi extract github.com/blushft/strana/processor 11 | -------------------------------------------------------------------------------- /processor/plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/blushft/strana/platform/config" 8 | "github.com/fatih/structs" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type PluginSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func TestRunPluginSuite(t *testing.T) { 17 | suite.Run(t, new(PluginSuite)) 18 | } 19 | 20 | func (s *PluginSuite) TestNewPlugin() { 21 | fp, err := filepath.Abs("./example") 22 | if err != nil { 23 | s.FailNow("get abs path", err) 24 | } 25 | 26 | conf := config.Processor{ 27 | Name: "myplugin", 28 | Type: "plugin", 29 | Options: structs.Map(Manifest{ 30 | Name: "test", 31 | Description: "testing", 32 | Version: "0.0.1", 33 | Type: "processor", 34 | Import: "example", 35 | Module: "example.go", 36 | Path: fp, 37 | Options: map[string]interface{}{ 38 | "name": "Test Plugin", 39 | }, 40 | }), 41 | } 42 | 43 | plug, err := New(conf) 44 | if err != nil { 45 | s.FailNow("create plugin", err) 46 | } 47 | 48 | _, err = plug.Process(nil) 49 | s.Require().NoError(err) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /processor/processor.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/platform/config" 8 | ) 9 | 10 | type EventProcessor interface { 11 | Process(*event.Event) ([]*event.Event, error) 12 | } 13 | 14 | type ProcessFunc func(*event.Event) ([]*event.Event, error) 15 | type ProcessorWrapper func(ProcessFunc) ProcessFunc 16 | 17 | type Constructor func(conf config.Processor) (EventProcessor, error) 18 | 19 | func Execute(procs []EventProcessor, evt *event.Event) ([]*event.Event, error) { 20 | q := []*event.Event{evt} 21 | 22 | for i := 0; len(q) > 0 && i < len(procs); i++ { 23 | var nextQ []*event.Event 24 | for _, m := range q { 25 | res, err := procs[i].Process(m) 26 | if err != nil { 27 | log.Printf("error executing processor: %s\n", err) 28 | return nil, err 29 | } 30 | 31 | nextQ = append(nextQ, res...) 32 | } 33 | 34 | q = nextQ 35 | } 36 | 37 | if len(q) == 0 { 38 | return nil, nil 39 | } 40 | 41 | return q, nil 42 | } 43 | -------------------------------------------------------------------------------- /processor/useragent/useragent.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | "github.com/blushft/strana/platform" 7 | "github.com/blushft/strana/platform/config" 8 | "github.com/blushft/strana/processor" 9 | ua "github.com/mileusna/useragent" 10 | ) 11 | 12 | func init() { 13 | platform.RegisterEventProcessor("useragent", func(config.Processor) (processor.EventProcessor, error) { 14 | return &uaproc{ 15 | validator: event.NewValidator( 16 | event.WithRule("has_network", event.HasContext(contexts.ContextNetwork)), 17 | event.WithRule("has_user_agent", event.ContextContains(contexts.ContextNetwork, "userAgent", true)), 18 | ), 19 | }, nil 20 | }) 21 | } 22 | 23 | type uaproc struct { 24 | validator event.Validator 25 | } 26 | 27 | func (proc *uaproc) Process(evt *event.Event) ([]*event.Event, error) { 28 | if !proc.validator.Validate(evt) { 29 | return []*event.Event{evt}, nil 30 | } 31 | 32 | v := evt.Context[string(contexts.ContextNetwork)].Interface() 33 | netctx := v.(*contexts.Network) 34 | eua := ua.Parse(netctx.UserAgent) 35 | 36 | bctx := &contexts.Browser{ 37 | Name: eua.Name, 38 | Version: eua.Version, 39 | UserAgent: netctx.UserAgent, 40 | } 41 | 42 | osctx := &contexts.OS{ 43 | Name: eua.OS, 44 | Version: eua.OSVersion, 45 | } 46 | 47 | devctx := &contexts.Device{ 48 | Mobile: eua.Mobile, 49 | Tablet: eua.Tablet, 50 | Desktop: eua.Desktop, 51 | } 52 | 53 | evt.SetContext(bctx) 54 | evt.SetContext(osctx) 55 | evt.SetContext(devctx) 56 | 57 | return []*event.Event{evt}, nil 58 | } 59 | -------------------------------------------------------------------------------- /strana.go: -------------------------------------------------------------------------------- 1 | package strana 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/platform/bus/message" 6 | "github.com/blushft/strana/platform/config" 7 | "github.com/blushft/strana/platform/logger" 8 | "github.com/blushft/strana/platform/store" 9 | "github.com/gofiber/fiber" 10 | ) 11 | 12 | type Module interface { 13 | Routes(fiber.Router) error 14 | Events(EventHandler) error 15 | Services(*store.SQLStore) error 16 | Logger(*logger.Logger) 17 | } 18 | 19 | type ModuleConstructor func(config.Module) (Module, error) 20 | 21 | type EventHandlerFunc func(*message.Message) ([]*message.Message, error) 22 | 23 | type EventHandler interface { 24 | Handle(src message.Path, sink message.Path, hndlr EventHandlerFunc) error 25 | Publisher() Publisher 26 | Subscriber() Subscriber 27 | } 28 | 29 | type Publisher interface { 30 | Publish(message.Path, *message.Message) error 31 | Close() error 32 | } 33 | 34 | type SubscriptionHandlerFunc func(*message.Message) error 35 | 36 | type Subscriber interface { 37 | Subscribe(message.Path, SubscriptionHandlerFunc) error 38 | Close() error 39 | } 40 | 41 | type Producer interface { 42 | Subscribe(SubscriptionHandlerFunc) error 43 | } 44 | 45 | type Consumer interface { 46 | Publish(*event.Event) error 47 | } 48 | 49 | type Processor interface { 50 | Module 51 | Producer 52 | Consumer 53 | } 54 | 55 | type Source interface { 56 | Module 57 | Producer 58 | } 59 | 60 | type Sink interface { 61 | Module 62 | Consumer 63 | } 64 | -------------------------------------------------------------------------------- /tracker/examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/blushft/strana/event" 8 | "github.com/blushft/strana/event/contexts" 9 | "github.com/blushft/strana/platform/logger" 10 | "github.com/blushft/strana/tracker" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | func main() { 15 | t, err := tracker.New( 16 | tracker.CollectorURL("http://localhost:7644"), 17 | tracker.TrackingID("1234"), 18 | tracker.SetAppInfo( 19 | &contexts.App{ 20 | Name: "Basic Example", 21 | Version: "v0.0.1", 22 | }, 23 | ), 24 | ) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | user := &contexts.User{ 30 | UserID: uuid.New().String(), 31 | Name: "fred", 32 | Traits: contexts.Traits{ 33 | Email: "fred@fakemail.int", 34 | }, 35 | } 36 | 37 | logger.Log().Info("identifying user") 38 | _ = t.Identify(user) 39 | 40 | logger.Log().Info("submitting action") 41 | _ = t.Action(&contexts.Action{ 42 | Category: "documents", 43 | Action: "open_document", 44 | Label: "doc", 45 | Property: "file_name", 46 | Value: "test_doc.txt", 47 | }) 48 | 49 | logger.Log().Info("start timing tracking") 50 | timer := t.TimingStart("downloads", "product_install", "download") 51 | 52 | time.Sleep(time.Millisecond * 5022) 53 | 54 | timing := timer.End() 55 | 56 | logger.Log().Info("submit timing") 57 | _ = t.Timing(timing, event.Channel("my_product")) 58 | 59 | time.Sleep(time.Second) 60 | 61 | logger.Log().Info("all done") 62 | } 63 | -------------------------------------------------------------------------------- /tracker/options.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "github.com/blushft/strana/event" 5 | "github.com/blushft/strana/event/contexts" 6 | ) 7 | 8 | type Options struct { 9 | CollectorURL string 10 | AppInfo *contexts.App 11 | Platform string 12 | TrackingID string 13 | QueueBuffer int 14 | } 15 | 16 | func defaultOptions(opts ...Option) Options { 17 | options := Options{ 18 | CollectorURL: "http://localhost:8863", 19 | QueueBuffer: 25, 20 | } 21 | 22 | for _, o := range opts { 23 | o(&options) 24 | } 25 | 26 | return options 27 | } 28 | 29 | type Option func(*Options) 30 | 31 | func (o Options) EventOptions() []event.Option { 32 | evtOpts := []event.Option{ 33 | event.WithContext(&contexts.Library{Name: "go_tracker", Version: "v0.0.1"}), 34 | } 35 | 36 | if o.AppInfo != nil { 37 | evtOpts = append(evtOpts, event.WithContext(o.AppInfo)) 38 | } 39 | 40 | if len(o.TrackingID) > 0 { 41 | evtOpts = append(evtOpts, event.TrackingID(o.TrackingID)) 42 | } 43 | 44 | if len(o.Platform) > 0 { 45 | evtOpts = append(evtOpts, event.Platform(o.Platform)) 46 | } else { 47 | evtOpts = append(evtOpts, event.Platform("srv")) 48 | } 49 | 50 | return evtOpts 51 | } 52 | 53 | func CollectorURL(u string) Option { 54 | return func(o *Options) { 55 | o.CollectorURL = u 56 | } 57 | } 58 | 59 | func SetAppInfo(app *contexts.App) Option { 60 | return func(o *Options) { 61 | o.AppInfo = app 62 | } 63 | } 64 | 65 | func TrackingID(id string) Option { 66 | return func(o *Options) { 67 | o.TrackingID = id 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tracker/store.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/blushft/strana/event" 10 | ) 11 | 12 | type StoreEvent struct { 13 | ID string 14 | Event []byte 15 | Attempted bool 16 | Attempts int 17 | LastAttempt time.Time 18 | } 19 | 20 | type Store interface { 21 | Set(*event.Event) error 22 | Update(*StoreEvent) error 23 | Remove(*StoreEvent) error 24 | Get(string) (*StoreEvent, error) 25 | GetAll() ([]*StoreEvent, error) 26 | } 27 | 28 | type memStore struct { 29 | m map[string][]byte 30 | mu sync.RWMutex 31 | } 32 | 33 | func NewMemStore() (Store, error) { 34 | return &memStore{ 35 | m: make(map[string][]byte), 36 | }, nil 37 | } 38 | 39 | func (s *memStore) Set(evt *event.Event) error { 40 | pl, err := json.Marshal(evt) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | se := &StoreEvent{ 46 | ID: evt.ID, 47 | Event: pl, 48 | Attempted: false, 49 | Attempts: 0, 50 | } 51 | 52 | return s.Update(se) 53 | } 54 | 55 | func (s *memStore) Update(se *StoreEvent) error { 56 | b, err := json.Marshal(se) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | s.mu.Lock() 62 | s.m[se.ID] = b 63 | s.mu.Unlock() 64 | 65 | return nil 66 | } 67 | 68 | func (s *memStore) Remove(se *StoreEvent) error { 69 | s.mu.Lock() 70 | delete(s.m, se.ID) 71 | s.mu.Unlock() 72 | 73 | return nil 74 | } 75 | 76 | func (s *memStore) Get(id string) (*StoreEvent, error) { 77 | s.mu.RLock() 78 | defer s.mu.RUnlock() 79 | 80 | b, ok := s.m[id] 81 | if !ok { 82 | return nil, errors.New("not found") 83 | } 84 | 85 | var se *StoreEvent 86 | if err := json.Unmarshal(b, &se); err != nil { 87 | return nil, err 88 | } 89 | 90 | return se, nil 91 | } 92 | 93 | func (s *memStore) GetAll() ([]*StoreEvent, error) { 94 | var ses []*StoreEvent 95 | 96 | for k := range s.m { 97 | se, err := s.Get(k) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | ses = append(ses, se) 103 | } 104 | 105 | return ses, nil 106 | } 107 | -------------------------------------------------------------------------------- /tracker/store_test.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/blushft/strana/event" 7 | "github.com/blushft/strana/event/contexts" 8 | "github.com/blushft/strana/event/events" 9 | "github.com/davecgh/go-spew/spew" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type StoreSuite struct { 14 | suite.Suite 15 | store Store 16 | } 17 | 18 | func TestStoreSuite(t *testing.T) { 19 | suite.Run(t, new(StoreSuite)) 20 | } 21 | 22 | func (s *StoreSuite) SetupSuite() { 23 | store, err := NewMemStore() 24 | if err != nil { 25 | s.FailNow(err.Error()) 26 | } 27 | 28 | s.store = store 29 | } 30 | 31 | func (s *StoreSuite) TestASet() { 32 | evt := event.New(events.EventTypeAction, 33 | event.TrackingID("123"), 34 | event.SessionID("321"), 35 | event.UserID("someguy"), 36 | event.WithContext(&contexts.Action{ 37 | Category: "test", 38 | }), 39 | ) 40 | 41 | spew.Dump(evt) 42 | 43 | s.Require().NoError(s.store.Set(evt)) 44 | 45 | se, err := s.store.Get(evt.ID) 46 | s.Require().NoError(err) 47 | 48 | spew.Dump(se) 49 | } 50 | 51 | func (s *StoreSuite) TestBGetAll() { 52 | se, err := s.store.GetAll() 53 | s.Require().NoError(err) 54 | 55 | s.Equal(1, len(se)) 56 | } 57 | -------------------------------------------------------------------------------- /tracker/timing.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | 8 | "github.com/blushft/strana/event/contexts" 9 | ) 10 | 11 | type timer struct { 12 | lock sync.Mutex 13 | start time.Time 14 | 15 | timing *contexts.Timing 16 | } 17 | 18 | func (t *Tracker) TimingStart(cat, label, variable string) *timer { 19 | return &timer{ 20 | start: time.Now(), 21 | timing: &contexts.Timing{ 22 | Category: cat, 23 | Label: label, 24 | Variable: variable, 25 | Unit: "seconds", 26 | Value: -1, 27 | }, 28 | } 29 | } 30 | 31 | func (timer *timer) End() *contexts.Timing { 32 | timer.lock.Lock() 33 | defer timer.lock.Unlock() 34 | dur := time.Now().Sub(timer.start).Seconds() 35 | 36 | timer.timing.Value = math.Floor(dur*100) / 100 37 | return timer.timing 38 | } 39 | -------------------------------------------------------------------------------- /tracker/tracker_test.go: -------------------------------------------------------------------------------- 1 | package tracker 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/blushft/strana/event/contexts" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type TrackerSuite struct { 12 | suite.Suite 13 | tr *Tracker 14 | } 15 | 16 | func TestRunTrackerSuite(t *testing.T) { 17 | suite.Run(t, new(TrackerSuite)) 18 | } 19 | 20 | func (s *TrackerSuite) SetupSuite() { 21 | tr, err := New(TrackingID("1234"), SetAppInfo(&contexts.App{ 22 | Name: "tracker_test", 23 | Version: "v0.1.0", 24 | })) 25 | s.Require().NoError(err) 26 | s.tr = tr 27 | } 28 | 29 | func (s *TrackerSuite) TearDownSuite() { 30 | s.tr.Close() 31 | } 32 | 33 | func (s *TrackerSuite) TestTrackAction() { 34 | a := &contexts.Action{ 35 | Category: "tests", 36 | Action: "Test Action", 37 | Label: "test_track", 38 | Property: "test_val", 39 | Value: 99, 40 | } 41 | 42 | s.Require().NoError(s.tr.Action(a)) 43 | 44 | time.Sleep(time.Second * 5) 45 | } 46 | -------------------------------------------------------------------------------- /xconfig.yaml: -------------------------------------------------------------------------------- 1 | debug: true 2 | database: 3 | dialect: sqlite 4 | database: strana.db 5 | server: 6 | host: 0.0.0.0 7 | port: 8863 8 | health: 9 | enabled: true 10 | path: /healthz 11 | # modules: 12 | # - name: collector 13 | # type: collector 14 | # sink: 15 | # topic: collected_raw_message 16 | # options: 17 | # type: tracker 18 | # cache: 19 | # default_expiration: 15 20 | # processors: 21 | # - name: ua 22 | # type: useragent 23 | # - name: logger 24 | # type: log 25 | # - name: enhancer 26 | # type: enhancer 27 | # source: 28 | # topic: collected_raw_message 29 | # sink: 30 | # topic: enhanced_raw_message 31 | # options: 32 | # processors: 33 | # - name: geoip 34 | # type: geoip 35 | # options: 36 | # database_path: "./.fixtures/geoip" 37 | # - name: loader 38 | # type: loader 39 | # source: 40 | # topic: enhanced_raw_message 41 | # - name: reporter 42 | # type: reporter 43 | # source: 44 | # topic: enhanced_raw_message 45 | 46 | 47 | --------------------------------------------------------------------------------