Giter Site home page Giter Site logo

novu-dotnet's Introduction

novu-dotnet

NuGet NuGet Deploy to Nuget

.NET SDK for Novu - The open-source notification infrastructure for engineers. 🚀

novu-dotnet targets .NET Standard 2.0 and is compatible with .NET Core 2.0+ and .NET Framework 4.6.1+.

Features

  • Bindings against most API endpoints
    • Events, subscribers, notifications, integrations, layouts, topics, workflows, workflow groups, messages, execution details
    • Not Implemented: environments, inbound parse, changes
  • Bootstrap each services as part of services provider or directly as a singleton class (setting injectable)
  • A Sync service that will mirror an environment based a set of templates (layouts, integrations, workflow groups, workflows)

WARNING: 0.3.0 has breaking changes and the tests should be relied on for understanding the client libraries

Dependencies

dotnet novu novu api package Notes
0.2.2 <= 0.17 Singleton client with Refit's use of RestService
0.3.0 >= 0.18 0.3.0 is not compatible with 0.2.2 and requires upgrade to code. Also 0.18 introduced a breaking change only found in 0.3.0. All 0.2.2 must be upgraded if used against the production system. HttpClient can now be used and injected.
0.3.1 >= 0.18 Failed release. You will not find this release on Nuget.
0.3.2 >= 0.18 [BREAKING} Obsolete Notification Templates has been removed. Service registration separation of single client and each client. Novu.Extension and Novu.Sync released as packages.

Installation

dotnet add package Novu

Configuration

Direct instantiation

using Novu.DTO;
using Novu.Models;
using Novu;

 var novuConfiguration = new NovuClientConfiguration
{
    // Defaults to https://api.novu.co/v1
    Url = "https://novu-api.my-domain.com/v1",
    ApiKey = "12345",
};

var novu = new NovuClient(novuConfiguration);

// Note: this client exposes all endpoints as methods but uses RestService
var subscribers = await novu.Subscriber.Get();

Dependency Injection

Configure via settings

{
  "Novu": {
    "Url": "http://localhost:3000/v1",
    "ApiKey": "e36b820fcc9a68a83db6c79c30f1a461"
  }
}

Setup Injection via extension methods

public static IServiceCollection RegisterNotificationSetupServices(
    this IServiceCollection services,
    IConfiguration configuration)
{
    // registers all clients with novu config from appsetting.json
    // the services inject HttpClient
    return services
        .RegisterNovuClients(configuration)
        // here as an example that the registered services are injected into local service
        .AddTransient<NovuNotificationService>();
}

Write your consuming code with the injected clients

// then instantiate via injection
public class NovuNotificationService
{
    private readonly IEventClient _event;

    public NovuSyncService(IEventClient @event)
    {
        _event = @event;
    }

    public async Task Trigger(string subscriberId){
       var trigger = await Event.Trigger(
            new EventCreateData
            {
                EventName = 'event-name',
                To = { SubscriberId =subscriberId },
                Payload = new Payload("Bogus payload"),
            });
    }

    public record Payload(string Message)
    {
        [JsonProperty("message")] public string Message { get; set; }
    }
}

Examples

Usage of the library is best understood by looking at the tests.

  • Integration Tests: these show the minimal dependencies required to do one primary request (create, update, delete)
  • Acceptance Tests: these show a sequence of actions to complete a business process in an environment

Repository Overview

Novu is the main SDK with Novu.Tests housing all unit tests. Novu.Extensions is required for DI and Novu.Sync if your are looking for mirroring environments.

novu-dotnet

The key folders to look into:

  • DTO directory holds all objects needed to use the clients
  • Interfaces directory holds all interfaces that are intended to outline how a class should be structured
  • Models directory holds various models that are sub-resources inside the DTOs

Major changes

Github issues closed

0.3.1

  • #57
  • #58
  • #59
  • #60
  • #55

0.3.0

  • #19
  • #20
  • #21
  • #34
  • #48
  • #49
  • #50
  • #47
  • #45

novu-dotnet's People

Contributors

benlin1994 avatar raphaelnagato avatar tlpl avatar toddb avatar unicodeveloper avatar wh1337 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

novu-dotnet's Issues

Refactor: Integrations require enumeration of providers inside the library

Providers are currently stringly known and undocumented.

The full list is here and should be introduced into the library.

However, C# doesn't implement enums with inheritance that you get with typescript. Either

  • implement a global provider list and attribute provider types (aka tag)
  • implement an enum-type factor provider

Note: validators could be put on the models and/or auto population of provider type based on name.

v0.2.2 broken on novu api 0.18.0 upgrade

The latest version SaaS version of Novu (cloud), and i'm using this package v0.2.2

The error i'm getting upon deserialising the response is

Refit.ApiException: An error occured deserializing the response.
 ---> Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[Novu.DTO.AdditionalDataDto]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'data._id', line 1, position 15.

but the part i find odd is this one Path 'data._id', line 1, position 15.. It seems that the entire response of the GetSubscriber is now wrapped in a data property (similarly what is happening with the CreateTopic endpoint where a TopicCreateResponseDto is returned )

Screenshot 2023-08-22 at 10 35 02

In summary seems that the response contract has changed for the Subscriber endpoints and now the actual response object is wrapped in a Data prop

Screenshot 2023-08-22 at 10 38 31 Screenshot 2023-08-22 at 10 38 50

So instead of returning a SubscriberDto should be returning it in an envelope, something like

class Foo {
    [JsonProperty("data")]
    public SubscriberDto Data { get; set; }
}
    [Get("/subscribers/{id}")]
    Task<Foo> GetSubscriber(string id);

    [Post("/subscribers")]
    Task<Foo> CreateSubscriber([Body] CreateSubscriberDto dto);

Originally posted by @joaonlforte in #46 (comment)

Use client code generation from novu open api specification

Hi all,
Since novu has openapi specification, we can use a tool like Kiota to automatically generate client that can be used to call the api. This will significantly reduce the time to build SDK that has full coverage of the available Novu API.
The only downside is that this will force us to rely on opinionated way to structure client and endpoints and how SDK client consume it. Though generated part can be made "internal" and public api will be handwritten on top of it.

Example sdk project structure is we will use client generation:
image

Example of generated client usage:
image

This generated client can be made internal and then we can build our own public contract on top of it. For example:
image

Separate out in DI the singleton Client from the explicit Clients

The RestService client is a single and wouldn't likely to be used at the same time as the explicit clients, so let people use only what they need.

Also the singleton client can remain backwards capatible but seems like the HttpClient (Pool) is going to need to be managed via injection.

Feature: Tenants

Current version doesn't have tenant support

  • Create trigger add tenant field
  • Add tenant endpoint

Method documentation enhancements

Each method should have detailed documentation, including in-line examples on how to implement the method. This will be helpful to keep the developer in their IDE versus having to go to the upcoming documentation site.

Local development setup for testing (and versioning against API)

I currently test this library using the docker-compose deployment script.

  • What do others use/do?
  • Should a copy be maintained here (self contained project and explicit "dependency")?
    • if so, should it run on "latest" or a specific version? (specific is most explicit)
    • if not, how can this project tie itself to the version of the API it can run against? (ie does it need to release its own version corresponding to the API?)

Versioning of the library

This library is tightly coupled to novu api versioning of the docker packages: see https://github.com/novuhq/novu/pkgs/container/novu%2Fapi

Tags are:

  • dev
  • prod
  • latest
  • version (eg 0.18.0)

Looking at the container repository:

  • dev is usually ahead of prod (not always)
  • prod looks to be always be latest
  • prod always looks to be ahead of the last version tag
  • prod is never tagged with a version <-- this seems strange IMHO

This library is tested before released on the production versioning of novu. Locally, it is tested on whatever version of the product the dev is using but most likely the docker-compose deployment script here. This script is not necessary up to date.

Final point, the developer build machine looks ideally to be a Mac which have arm64 yet the builds are amd64.

Furthermore, releases to production are both un-notified (eg in Discord) and not tagged.

Suggestions to versioning:

  • have a copy of the docker-compose that has the version of the product tested against
  • document clearly in the README the version tested against
  • this library MAY need to follow versioning conventions so that when in nuget developers can see related upgrades (however, this convention is usual when the library is based on another nuget package and particularly a framework-type package)

Setup GitHub Workflow for Running Unit Tests

In order to ensure the stability and reliability of our codebase, it is essential to have automated unit tests running for each push and pull request. Currently, our repository lacks a GitHub Actions workflow to run unit tests, and this needs to be addressed to maintain code quality and catch issues early.

Suggested Solution:

  1. Create a new workflow file in .github/workflows/ directory.
    Filename: run-unit-tests.yml

  2. Configure the workflow to be triggered on every push and pull_request event to the main branch.

  3. Define job steps:
    Checkout the repository.
    Setup the environment/java runtime.
    Install dependencies.
    Run the unit tests.

INovuClientConfiguration is not exposed via DI service provider

The values here are required for services that provide these details out to clients. For example, the server API provides clients with the resolution of the novu client for subscriber notifications (rather than hard code into the client)

Fix: register the interface for DI:

// note: IConfiguration configuration (because you are in registration land)
services.AddTransient<INovuClientConfiguration>(_ => configuration.GetNovuClientConfiguration())

Feature: Sync "templates" with the API for environment provisioning (eg Layouts, Workflow, Integrations)

To the authors, would you be interested in this project having a little scope creep and be more than bindings against the API and provide provisioning functionality using the library itself?

In Novu, others ask about programmatic provisioning (eg Layout, Workflows, Integrations) and there are currently no solutions available and I assume most people do it manually. The web interface does it via "promoting" which is not what many are wanting.

This library could provide client code that can be run against the server to do a diff (create, delete, update) based on the provided set of templates. It is workable as an approach (honestly, until someone writes a terraform provider IMHO) for people who are going to use Novu in a build pipeline and standup separate instances per environment.

I believe it should be in the repo because it is coupled to this code. It could be published as a separate package (or not). It should elevate the status of this library too. It could be maintained separately but that just seems a pain for updates.

Thoughts?

chore: GitHub Actions

  • Auto deployments to Nuget when a new release is generated tagged with version
  • Test action on each PR

Add Exponential Retry Mechanism with Idempotency Headers

In order to enhance the resilience and reliability of our SDK, we would like to introduce an Exponential Retry mechanism for retrying failed requests. Additionally, to ensure the idempotent processing of requests, it's vital to incorporate support for providing an Idempotency Key as per the draft specified in the HTTP Idempotency Key Header Field.

The key requirements for this implementation include:

  1. Exponential Retry Mechanism:

    • The SDK should retry failed requests following an exponential backoff strategy to minimize the contention and impact on the systems involved.
    • The SDK should ensure that the retry mechanism is configurable (e.g., max retries, initial delay, maximum delay).
  2. Idempotency Key Provisioning:

    • The SDK should allow for either automatic or manual provisioning of an Idempotency Key for each request.
    • The Idempotency Key should conform to either CUID, ULID, or UUID formats as specified in the draft.
    • The Idempotency Key should be included in the HTTP Header as Idempotency-Key and following the standards outlined in the draft.
  3. Configuration and Documentation:

    • The SDK should provide configuration options for enabling/disabling the Exponential Retry mechanism and Idempotency Key provisioning.
    • Comprehensive documentation should be provided explaining the configuration options, operational behavior, and the benefits of using the Exponential Retry mechanism along with Idempotency Keys.

Acceptance Criteria:

  • Implementation of the Exponential Retry mechanism with configurable parameters.
  • Provisioning of Idempotency Keys, either automatically or manually, conforming to specified formats (CUID, ULID, or UUID).
  • Adequate unit and integration testing to ensure the robustness and reliability of the implemented features.
  • Comprehensive documentation on the usage and configuration of the Exponential Retry mechanism and Idempotency Key provisioning.
  • Adherence to the specifications outlined in the HTTP Idempotency Key Header Field draft.

Update: You can reference the go-lang library to keep the method signature and configuration the same.
novuhq/go-novu#62

Please refer to the draft for further details on the HTTP Idempotency Key Header Field and ensure adherence to the specified standards while implementing this feature in the SDK.

[Docs]:- Adding Contributors section to Readme.md

A "Contributors" section in a README is important because it acknowledges contributors, promotes transparency, encourages collaboration, builds a community, and aids in project accountability.

Example:-
3

Project organization / structure / conventions

Hi, thanks for creating this project!

I'd like discuss the following things about how it's being built:

  1. Project structure.
    Atm, the main sdk project is structured per item type - Exceptions, Models etc. While it one of the ways to go I believe it's more future-proof to structure it per feature.
    Example:
    image
    Imo, this improves cohesion in the code - related pieces live together.

  2. Naming and conventions
    Since this is an SDK I don't think the need to name things with DTO suffix. As that is mainly used to separate domain entities and public contracts when building services. For example: User -> UserDto.
    In case of this sdk types like Subscriber are part of the domain of SDK.
    Also, imo, removing DTO suffix makes it more readable.

  3. RestSharp vs Refit
    There is a tool called Refit that can be used to do http calls to novu instead of RestSharp. As shown here https://www.reddit.com/r/dotnet/comments/11fcxdx/dotnetbenchmark_refit_vs_restsharp/ it has slightly better performance. Also it uses source generators underneath so removes quite a lot boilerplate code which will reduce the overall size of this sdk.

I'll happily contribute to addressing these 3 topics if they make sense for the team. Cheers

Missing methods

Some endpoints of the following section can't be hit from this SDK. Please ensure that all the SDK methods are up to date. I'm adding here a list of sections to check/update:

  • Notification templates
  • Integrations
  • Layouts
  • Inbound Parse
  • Environments
  • Changes
  • Execution details
  • Feeds
  • Messages
  • Blueprints

Some of these methods already exist. No need to change/update them. This is just to bring every SDK we have on par with all the methods available.

Check all the endpoints here and add the missing ones.

Cannot desrialize Subscriber if they have custom data using the .net sdk for single endpoint

if you try to use the single endpoint or create subscriber the with added key value in the data field the data won't deserialize but it will work with Multiple endpoint
Refit.ApiException: An error occured deserializing the response.
---> Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Novu.DTO.Subscribers.Subscriber' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path 'data', line 1, position 235.
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureArrayContract(JsonReader reader, Type objectType, JsonContract contract)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor1 creator, String id) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonSerializer.Deserialize[T](JsonReader reader) at Refit.NewtonsoftJsonContentSerializer.FromHttpContentAsync[T](HttpContent content, CancellationToken cancellationToken) in /_/Refit.Newtonsoft.Json/NewtonsoftJsonContentSerializer.cs:line 56 at Refit.RequestBuilderImplementation.DeserializeContentAsync[T](HttpResponseMessage resp, HttpContent content, CancellationToken cancellationToken) at Refit.RequestBuilderImplementation.<>c__DisplayClass14_02.<b__0>d.MoveNext()
--- End of inner exception stack trace ---
at Refit.RequestBuilderImplementation.<>c__DisplayClass14_0`2.<b__0>d.MoveNext() in /_/Refit/RequestBuilderImplementation.cs:line 298
--- End of stack trace from previous location ---
at NovuService.Novu.NovuController.GetByIdAsync(String id) in C:\Backup\NovuService\NovuService\Novu\NovuController.cs:line 27
at lambda_method5(Closure , Object )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Add User Agent Header

Add the user-agent request header to every call in this sdk.

Example:

headers: {
Authorization: Bearer ${ApiKey},
"User-Agent": novu/${sdk}@${version},
},

Why? (Context)

Having request headers in the SDKs will help us in monitoring which SDK is mostly used by the community and customers. It will help us make the decisions in the future on which SDKs to officially support in the future.

Release Extensions and Sync dlls

Novu has three parts/dlls:

  • Novu (core—standalone client bindings with Refit as the primary across-the-wire client—not necessarily the http client (and Json.net)—future warning that this library is now bound to the dependency upgrade policies of Refit (for example))
  • Extensions (extension methods that bring in additional dependencies like IOC (Microsoft.DependencyInjection))
  • Sync (library for mirroring environments from templates)

There is a decision:

  • release all together
  • release separately

The code as part of 0.3.0 did not release this code.

Note: Novu may need to be called Novu.Core (or the like) indicating base.

Refactor: Client Interface and Model (change pattern conventions)

Up to client 0.2.x, each Client interface based on Refit used the naming convention of the Novu API source code:

  • "Dto"
  • "Request"/"Response"
  • End points have verb and noun (eg GetSubscribers)
  • Query params were separated out as params on methods
  • Also, inconsistencies around the "envelope" with Data and pagination
  • Multiple classes per files (one class per file)

Goals

  • Client (Http) Endpoints
    • generally avoid using nouns and use the http verb or object based equivalent
    • use method signatures for uniqueness over method name
    • sub-resource endpoints may have a noun as well (at this stage we aren't going to decompose/re-abstract the api itself)
    • All methods are async and no sync (no need for async convention—this is/was a Microsoft transition approach)
  • Model conventions
    • singleton representation/resource/object/model as the name of the domain model: Workflow, WorkflowGroup
    • collection representations as IEnumerable
    • Create and edit representations (over the wire) with suffix: CreateData and EditData (eg WorkflowEditData)
  • Envelope wrappers (note: hate the names and this may be able to be further reduced as T can be singleton or collection)
    • NovuResponse
    • NovuPaginationResponse
  • QueryParams
    • Refactor params to class
    • Potentially retain params as well

Feature: Setup docfx

docfx is a tool that will autogenerate a GitHub pages compatible website that auto documents the SDK. This allows for a single place to document, without the need for a separate repository or commits to set up documentation.

Site for additional details and guides: https://dotnet.github.io/docfx/

Cannot set layout on a step (email)

Unable to set the layout on an email step inside a layout. More technically, the Layout (id) is set on the MessageTemplate on a step.

Notes:

  • API does not set the Default layout on creation (yet, it mutates the POST with say variables)
  • The web interface makes it look like the email step has a default layout! (but is null in the background)
  • The underlying implementation is that ALL step types CAN have a layout—however the web ui only exposes email types through the Brand > Layout section

Environment variable NOVU_API_KEY is not injected (as an override)

Release 0.3.0 did not make NOVU_API_KEY injectable into the configuration.

Currently only NOVU_API_URL is injected. Note the the build pipepline gets around this by injecting in the appsettings.json

see ConfigurationExtensions


        var novuConfigurationApiKey = Environment.GetEnvironmentVariable("NOVU_API_KEY");
        if (novuConfigurationApiKey is not null)
        {
            novuConfiguration.ApiKey = novuConfigurationApiKey;
        }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.